Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
509e0441
Unverified
提交
509e0441
authored
5月 29, 2025
作者:
Will Chen
提交者:
GitHub
5月 29, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Boilerplate free tests (#277)
上级
7a4ec224
显示空白字符变更
内嵌
并排
正在显示
13 个修改的文件
包含
197 行增加
和
180 行删除
+197
-180
ci.yml
.github/workflows/ci.yml
+2
-0
.gitignore
.gitignore
+3
-0
.oxlintrc.json
.oxlintrc.json
+2
-1
.prettierignore
.prettierignore
+2
-0
codegen.js
e2e-tests/helpers/codegen.js
+24
-0
test_helper.ts
e2e-tests/helpers/test_helper.ts
+78
-0
main.spec.ts
e2e-tests/main.spec.ts
+47
-86
main.spec.ts_simple-message-to-custom-test-model-1.aria.yml
...in.spec.ts_simple-message-to-custom-test-model-1.aria.yml
+17
-0
package-lock.json
package-lock.json
+2
-2
playwright.config.ts
playwright.config.ts
+11
-0
MessagesList.tsx
src/components/chat/MessagesList.tsx
+5
-1
index.js
testing/fake-llm-server/dist/index.js
+0
-90
index.ts
testing/fake-llm-server/index.ts
+4
-0
没有找到文件。
.github/workflows/ci.yml
浏览文件 @
509e0441
...
@@ -41,6 +41,8 @@ jobs:
...
@@ -41,6 +41,8 @@ jobs:
run
:
npx playwright install chromium --with-deps
run
:
npx playwright install chromium --with-deps
-
name
:
Build
-
name
:
Build
run
:
npm run pre:e2e
run
:
npm run pre:e2e
-
name
:
Prep test server
run
:
cd testing/fake-llm-server && npm install && npm run build && cd -
-
name
:
E2E tests
-
name
:
E2E tests
# Add debug logging to make it easier to see what's failing
# Add debug logging to make it easier to see what's failing
run
:
DEBUG=pw:browser npm run e2e
run
:
DEBUG=pw:browser npm run e2e
...
...
.gitignore
浏览文件 @
509e0441
...
@@ -6,6 +6,9 @@ yarn-debug.log*
...
@@ -6,6 +6,9 @@ yarn-debug.log*
yarn-error.log*
yarn-error.log*
lerna-debug.log*
lerna-debug.log*
# dist
dist/
# playwright
# playwright
playwright-report/
playwright-report/
test-results/
test-results/
...
...
.oxlintrc.json
浏览文件 @
509e0441
{
{
"rules"
:
{
"rules"
:
{
"eslint/no-unused-vars"
:
"error"
"eslint/no-unused-vars"
:
"error"
,
"eslint/no-empty-pattern"
:
"off"
}
}
}
}
.prettierignore
浏览文件 @
509e0441
...
@@ -4,3 +4,4 @@ coverage
...
@@ -4,3 +4,4 @@ coverage
# generated files
# generated files
drizzle/
drizzle/
**/pnpm-lock.yaml
**/pnpm-lock.yaml
**/snapshots/**
\ No newline at end of file
e2e-tests/helpers/codegen.js
0 → 100644
浏览文件 @
509e0441
/*
* From: https://github.com/microsoft/playwright/issues/5181#issuecomment-2769098576
*
* Usage:
* cd e2e-tests/helpers && node codegen.js
*/
const
{
_electron
:
electron
}
=
require
(
"playwright"
);
(
async
()
=>
{
const
browser
=
await
electron
.
launch
({
args
:
[
"../../out/dyad-darwin-arm64/dyad.app/Contents/Resources/app.asar/.vite/build/main.js"
,
"--enable-logging"
,
"--user-data-dir=/tmp/dyad-e2e-tests"
,
],
executablePath
:
"../../out/dyad-darwin-arm64/dyad.app/Contents/MacOS/dyad"
,
});
const
context
=
await
browser
.
context
();
await
context
.
route
(
"**/*"
,
(
route
)
=>
route
.
continue
());
await
require
(
"node:timers/promises"
).
setTimeout
(
3000
);
// wait for the window to load
await
browser
.
windows
()[
0
].
pause
();
// .pause() opens the Playwright-Inspector for manual recording
})();
e2e-tests/helpers/test_helper.ts
0 → 100644
浏览文件 @
509e0441
import
{
test
as
base
}
from
"@playwright/test"
;
import
{
findLatestBuild
,
parseElectronApp
}
from
"electron-playwright-helpers"
;
import
{
ElectronApplication
,
_electron
as
electron
}
from
"playwright"
;
const
showDebugLogs
=
process
.
env
.
DEBUG_LOGS
===
"true"
;
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
//
// Note how we mark the fixture as { auto: true }.
// This way it is always instantiated, even if the test does not use it explicitly.
export
const
test
=
base
.
extend
<
{
attachScreenshotsToReport
:
void
;
electronApp
:
ElectronApplication
;
}
>
({
attachScreenshotsToReport
:
[
async
({
page
},
use
,
testInfo
)
=>
{
await
use
();
// After the test we can check whether the test passed or failed.
if
(
testInfo
.
status
!==
testInfo
.
expectedStatus
)
{
const
screenshot
=
await
page
.
screenshot
();
await
testInfo
.
attach
(
"screenshot"
,
{
body
:
screenshot
,
contentType
:
"image/png"
,
});
}
},
{
auto
:
true
},
],
electronApp
:
async
({},
use
)
=>
{
// find the latest build in the out directory
const
latestBuild
=
findLatestBuild
();
// parse the directory and find paths and other info
const
appInfo
=
parseElectronApp
(
latestBuild
);
process
.
env
.
E2E_TEST_BUILD
=
"true"
;
// This is just a hack to avoid the AI setup screen.
process
.
env
.
OPENAI_API_KEY
=
"sk-test"
;
const
electronApp
=
await
electron
.
launch
({
args
:
[
appInfo
.
main
,
"--enable-logging"
,
`--user-data-dir=/tmp/dyad-e2e-tests-
${
Date
.
now
()}
`
,
],
executablePath
:
appInfo
.
executable
,
});
console
.
log
(
"electronApp launched!"
);
if
(
showDebugLogs
)
{
// Listen to main process output immediately
electronApp
.
process
().
stdout
?.
on
(
"data"
,
(
data
)
=>
{
console
.
log
(
`MAIN_PROCESS_STDOUT:
${
data
.
toString
()}
`
);
});
electronApp
.
process
().
stderr
?.
on
(
"data"
,
(
data
)
=>
{
console
.
error
(
`MAIN_PROCESS_STDERR:
${
data
.
toString
()}
`
);
});
}
electronApp
.
on
(
"close"
,
()
=>
{
console
.
log
(
`Electron app closed listener:`
);
});
electronApp
.
on
(
"window"
,
async
(
page
)
=>
{
const
filename
=
page
.
url
()?.
split
(
"/"
).
pop
();
console
.
log
(
`Window opened:
${
filename
}
`
);
// capture errors
page
.
on
(
"pageerror"
,
(
error
)
=>
{
console
.
error
(
error
);
});
// capture console messages
page
.
on
(
"console"
,
(
msg
)
=>
{
console
.
log
(
msg
.
text
());
});
});
await
use
(
electronApp
);
await
electronApp
.
close
();
},
});
e2e-tests/main.spec.ts
浏览文件 @
509e0441
/**
import
{
expect
}
from
"@playwright/test"
;
* Example Playwright script for Electron
import
{
test
}
from
"./helpers/test_helper"
;
* showing/testing various API features
* in both renderer and main processes
*/
import
{
expect
,
test
as
base
}
from
"@playwright/test"
;
test
(
"renders the first page"
,
async
({
electronApp
})
=>
{
import
{
findLatestBuild
,
parseElectronApp
}
from
"electron-playwright-helpers"
;
const
page
=
await
electronApp
.
firstWindow
();
import
{
ElectronApplication
,
Page
,
_electron
as
electron
}
from
"playwright"
;
let
electronApp
:
ElectronApplication
;
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
//
// Note how we mark the fixture as { auto: true }.
// This way it is always instantiated, even if the test does not use it explicitly.
export
const
test
=
base
.
extend
<
{
attachScreenshotsToReport
:
void
}
>
({
attachScreenshotsToReport
:
[
async
({},
use
,
testInfo
)
=>
{
await
use
();
// After the test we can check whether the test passed or failed.
if
(
testInfo
.
status
!==
testInfo
.
expectedStatus
)
{
const
screenshot
=
await
page
.
screenshot
();
await
testInfo
.
attach
(
"screenshot"
,
{
body
:
screenshot
,
contentType
:
"image/png"
,
});
}
},
{
auto
:
true
},
],
});
test
.
beforeAll
(
async
()
=>
{
// find the latest build in the out directory
const
latestBuild
=
findLatestBuild
();
// parse the directory and find paths and other info
const
appInfo
=
parseElectronApp
(
latestBuild
);
process
.
env
.
E2E_TEST_BUILD
=
"true"
;
// This is just a hack to avoid the AI setup screen.
process
.
env
.
OPENAI_API_KEY
=
"sk-test"
;
electronApp
=
await
electron
.
launch
({
args
:
[
appInfo
.
main
,
"--enable-logging"
,
"--user-data-dir=/tmp/dyad-e2e-tests"
,
],
executablePath
:
appInfo
.
executable
,
});
console
.
log
(
"electronApp launched!"
);
// Listen to main process output immediately
electronApp
.
process
().
stdout
?.
on
(
"data"
,
(
data
)
=>
{
console
.
log
(
`MAIN_PROCESS_STDOUT:
${
data
.
toString
()}
`
);
});
electronApp
.
process
().
stderr
?.
on
(
"data"
,
(
data
)
=>
{
console
.
error
(
`MAIN_PROCESS_STDERR:
${
data
.
toString
()}
`
);
});
electronApp
.
on
(
"close"
,
()
=>
{
console
.
log
(
`Electron app closed listener:`
);
});
electronApp
.
on
(
"window"
,
async
(
page
)
=>
{
const
filename
=
page
.
url
()?.
split
(
"/"
).
pop
();
console
.
log
(
`Window opened:
${
filename
}
`
);
// capture errors
page
.
on
(
"pageerror"
,
(
error
)
=>
{
console
.
error
(
error
);
});
// capture console messages
page
.
on
(
"console"
,
(
msg
)
=>
{
console
.
log
(
msg
.
text
());
});
});
});
test
.
afterAll
(
async
()
=>
{
await
electronApp
.
close
();
});
let
page
:
Page
;
test
(
"renders the first page"
,
async
()
=>
{
page
=
await
electronApp
.
firstWindow
();
await
page
.
waitForSelector
(
"h1"
);
await
page
.
waitForSelector
(
"h1"
);
const
text
=
await
page
.
$eval
(
"h1"
,
(
el
)
=>
el
.
textContent
);
const
text
=
await
page
.
$eval
(
"h1"
,
(
el
)
=>
el
.
textContent
);
expect
(
text
).
toBe
(
"Build your dream app"
);
expect
(
text
).
toBe
(
"Build your dream app"
);
});
});
test
(
"simple message to custom test model"
,
async
({
electronApp
})
=>
{
const
page
=
await
electronApp
.
firstWindow
();
await
page
.
getByRole
(
"link"
,
{
name
:
"Settings"
}).
click
();
await
page
.
getByText
(
"Add custom providerConnect to"
).
click
();
// Fill out provider dialog
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Provider ID"
}).
fill
(
"testing"
);
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Display Name"
}).
click
();
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Display Name"
})
.
fill
(
"test-provider"
);
await
page
.
getByText
(
"API Base URLThe base URL for"
).
click
();
await
page
.
getByRole
(
"textbox"
,
{
name
:
"API Base URL"
})
.
fill
(
"http://localhost:3500/v1"
);
await
page
.
getByRole
(
"button"
,
{
name
:
"Add Provider"
}).
click
();
// Create custom model
await
page
.
getByRole
(
"heading"
,
{
name
:
"test-provider Needs Setup"
})
.
click
();
await
page
.
getByRole
(
"button"
,
{
name
:
"Add Custom Model"
}).
click
();
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Model ID*"
}).
fill
(
"test-model"
);
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Model ID*"
}).
press
(
"Tab"
);
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Name*"
}).
fill
(
"test-model"
);
await
page
.
getByRole
(
"button"
,
{
name
:
"Add Model"
}).
click
();
// Go to apps page and select custom model
await
page
.
getByRole
(
"link"
,
{
name
:
"Apps"
}).
click
();
await
page
.
getByRole
(
"button"
,
{
name
:
"Model: Auto"
}).
click
();
await
page
.
getByText
(
"test-provider"
).
click
();
await
page
.
getByText
(
"test-model"
).
click
();
// Enter prompt and send
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Ask Dyad to build..."
}).
click
();
await
page
.
getByRole
(
"textbox"
,
{
name
:
"Ask Dyad to build..."
}).
fill
(
"hi"
);
await
page
.
getByRole
(
"button"
,
{
name
:
"Start new chat"
}).
click
();
// Make sure it's done
await
expect
(
page
.
getByRole
(
"button"
,
{
name
:
"Retry"
})).
not
.
toBeVisible
();
await
expect
(
page
.
getByTestId
(
"messages-list"
)).
toMatchAriaSnapshot
();
});
e2e-tests/snapshots/main.spec.ts_simple-message-to-custom-test-model-1.aria.yml
0 → 100644
浏览文件 @
509e0441
-
paragraph
:
hi
-
'
button
"Thinking
`<dyad-write>`:
I'
'
ll
think
about
the
problem
and
write
a
bug
report.
<dyad-write>
<dyad-write
path=\"file1.txt\">
Fake
dyad
write
</dyad-write>"'
:
-
img
-
img
-
paragraph
:
-
code
:
"
`<dyad-write>`"
-
text
:
"
:
I'll
think
about
the
problem
and
write
a
bug
report."
-
paragraph
:
<dyad-write>
-
paragraph
:
<dyad-write path="file1.txt"> Fake dyad write </dyad-write>
-
img
-
text
:
file1.txt
-
img
-
text
:
file1.txt
-
paragraph
:
More EOM
-
button "Retry"
:
-
img
\ No newline at end of file
package-lock.json
浏览文件 @
509e0441
{
{
"name"
:
"dyad"
,
"name"
:
"dyad"
,
"version"
:
"0.
6
.0"
,
"version"
:
"0.
7
.0"
,
"lockfileVersion"
:
3
,
"lockfileVersion"
:
3
,
"requires"
:
true
,
"requires"
:
true
,
"packages"
:
{
"packages"
:
{
""
:
{
""
:
{
"name"
:
"dyad"
,
"name"
:
"dyad"
,
"version"
:
"0.
6
.0"
,
"version"
:
"0.
7
.0"
,
"license"
:
"MIT"
,
"license"
:
"MIT"
,
"dependencies"
:
{
"dependencies"
:
{
"@ai-sdk/anthropic"
:
"^1.2.8"
,
"@ai-sdk/anthropic"
:
"^1.2.8"
,
...
...
playwright.config.ts
浏览文件 @
509e0441
...
@@ -3,6 +3,12 @@ import { PlaywrightTestConfig } from "@playwright/test";
...
@@ -3,6 +3,12 @@ import { PlaywrightTestConfig } from "@playwright/test";
const
config
:
PlaywrightTestConfig
=
{
const
config
:
PlaywrightTestConfig
=
{
testDir
:
"./e2e-tests"
,
testDir
:
"./e2e-tests"
,
maxFailures
:
1
,
maxFailures
:
1
,
timeout
:
process
.
env
.
CI
?
30
_000
:
10
_000
,
// Use a custom snapshot path template because Playwright's default
// is platform-specific which isn't necessary for Dyad e2e tests
// which should be platform agnostic (we don't do screenshots; only textual diffs).
snapshotPathTemplate
:
"{testDir}/{testFileDir}/snapshots/{testFileName}_{arg}{ext}"
,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter
:
"html"
,
reporter
:
"html"
,
...
@@ -18,6 +24,11 @@ const config: PlaywrightTestConfig = {
...
@@ -18,6 +24,11 @@ const config: PlaywrightTestConfig = {
// screenshot: "on",
// screenshot: "on",
// video: "retain-on-failure",
// video: "retain-on-failure",
},
},
webServer
:
{
command
:
`cd testing/fake-llm-server && npm start`
,
url
:
"http://localhost:3500/health"
,
},
};
};
export
default
config
;
export
default
config
;
src/components/chat/MessagesList.tsx
浏览文件 @
509e0441
...
@@ -34,7 +34,11 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
...
@@ -34,7 +34,11 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
const
selectedChatId
=
useAtomValue
(
selectedChatIdAtom
);
return
(
return
(
<
div
className=
"flex-1 overflow-y-auto p-4"
ref=
{
ref
}
>
<
div
className=
"flex-1 overflow-y-auto p-4"
ref=
{
ref
}
data
-
testid=
"messages-list"
>
{
messages
.
length
>
0
?
(
{
messages
.
length
>
0
?
(
messages
.
map
((
message
,
index
)
=>
(
messages
.
map
((
message
,
index
)
=>
(
<
ChatMessage
<
ChatMessage
...
...
testing/fake-llm-server/dist/index.js
deleted
100644 → 0
浏览文件 @
7a4ec224
"use strict"
;
var
__importDefault
=
(
this
&&
this
.
__importDefault
)
||
function
(
mod
)
{
return
mod
&&
mod
.
__esModule
?
mod
:
{
default
:
mod
};
};
Object
.
defineProperty
(
exports
,
"__esModule"
,
{
value
:
true
});
const
express_1
=
__importDefault
(
require
(
"express"
));
const
http_1
=
require
(
"http"
);
const
cors_1
=
__importDefault
(
require
(
"cors"
));
// Create Express app
const
app
=
(
0
,
express_1
.
default
)();
app
.
use
((
0
,
cors_1
.
default
)());
app
.
use
(
express_1
.
default
.
json
());
const
PORT
=
3500
;
// Helper function to create OpenAI-like streaming response chunks
function
createStreamChunk
(
content
,
role
=
"assistant"
,
isLast
=
false
)
{
const
chunk
=
{
id
:
`chatcmpl-
${
Date
.
now
()}
`
,
object
:
"chat.completion.chunk"
,
created
:
Math
.
floor
(
Date
.
now
()
/
1000
),
model
:
"fake-model"
,
choices
:
[
{
index
:
0
,
delta
:
isLast
?
{}
:
{
content
,
role
},
finish_reason
:
isLast
?
"stop"
:
null
,
},
],
};
return
`data:
${
JSON
.
stringify
(
chunk
)}
\n\n
${
isLast
?
"data: [DONE]
\
n
\
n"
:
""
}
`
;
}
// Handle POST requests to /v1/chat/completions
app
.
post
(
"/v1/chat/completions"
,
(
req
,
res
)
=>
{
const
{
stream
=
false
}
=
req
.
body
;
// Non-streaming response
if
(
!
stream
)
{
return
res
.
json
({
id
:
`chatcmpl-
${
Date
.
now
()}
`
,
object
:
"chat.completion"
,
created
:
Math
.
floor
(
Date
.
now
()
/
1000
),
model
:
"fake-model"
,
choices
:
[
{
index
:
0
,
message
:
{
role
:
"assistant"
,
content
:
"hello world"
,
},
finish_reason
:
"stop"
,
},
],
});
}
// Streaming response
res
.
setHeader
(
"Content-Type"
,
"text/event-stream"
);
res
.
setHeader
(
"Cache-Control"
,
"no-cache"
);
res
.
setHeader
(
"Connection"
,
"keep-alive"
);
// Split the "hello world" message into characters to simulate streaming
const
message
=
"hello world"
;
const
messageChars
=
message
.
split
(
""
);
// Stream each character with a delay
let
index
=
0
;
// Send role first
res
.
write
(
createStreamChunk
(
""
,
"assistant"
));
const
interval
=
setInterval
(()
=>
{
if
(
index
<
messageChars
.
length
)
{
res
.
write
(
createStreamChunk
(
messageChars
[
index
]));
index
++
;
}
else
{
// Send the final chunk
res
.
write
(
createStreamChunk
(
""
,
"assistant"
,
true
));
clearInterval
(
interval
);
res
.
end
();
}
},
100
);
});
// Start the server
const
server
=
(
0
,
http_1
.
createServer
)(
app
);
server
.
listen
(
PORT
,
()
=>
{
console
.
log
(
`Fake LLM server running on http://localhost:
${
PORT
}
`
);
});
// Handle SIGINT (Ctrl+C)
process
.
on
(
"SIGINT"
,
()
=>
{
console
.
log
(
"Shutting down fake LLM server"
);
server
.
close
(()
=>
{
console
.
log
(
"Server closed"
);
process
.
exit
(
0
);
});
});
testing/fake-llm-server/index.ts
浏览文件 @
509e0441
...
@@ -50,6 +50,10 @@ const CANNED_MESSAGE = `
...
@@ -50,6 +50,10 @@ const CANNED_MESSAGE = `
More
More
EOM`
;
EOM`
;
app
.
get
(
"/health"
,
(
req
,
res
)
=>
{
res
.
send
(
"OK"
);
});
// Handle POST requests to /v1/chat/completions
// Handle POST requests to /v1/chat/completions
app
.
post
(
"/v1/chat/completions"
,
(
req
,
res
)
=>
{
app
.
post
(
"/v1/chat/completions"
,
(
req
,
res
)
=>
{
const
{
stream
=
false
,
messages
=
[]
}
=
req
.
body
;
const
{
stream
=
false
,
messages
=
[]
}
=
req
.
body
;
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论