fix(test): e2e stability (#14749)

#### PR Dependency Tree


* **PR #14749** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved link preview reliability by updating request identification
to better match modern browsers.
* **Tests**
* Made end-to-end and integration tests deterministic and more robust,
improving AI chat, image generation, attachment handling, settings
visibility, and editor flows.
* **Chores**
* Updated underlying tooling versions to enhance stability and
compatibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-29 23:26:15 +08:00
committed by GitHub
parent 9ca520a81f
commit 91ad783973
12 changed files with 136 additions and 114 deletions
@@ -13,6 +13,26 @@ function expectPxCloseTo(
expect(Number.parseFloat(value)).toBeCloseTo(expected, precision);
}
async function waitForShapeElement(
surfaceView: ReturnType<typeof getSurface>,
shapeId: string,
timeout = 1000
) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeout) {
const shapeElement = surfaceView.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
if (shapeElement) return shapeElement;
await wait(50);
}
return null;
}
describe('Shape rendering with DOM renderer', () => {
beforeEach(async () => {
const cleanup = await setupEditor('edgeless', [], {
@@ -40,10 +60,7 @@ describe('Shape rendering with DOM renderer', () => {
};
const shapeId = surfaceModel.addElement(shapeProps);
await new Promise(resolve => setTimeout(resolve, 100));
const shapeElement = surfaceView?.renderRoot.querySelector(
`[data-element-id="${shapeId}"]`
);
const shapeElement = await waitForShapeElement(surfaceView, shapeId);
expect(shapeElement).not.toBeNull();
expect(shapeElement).toBeInstanceOf(HTMLElement);
@@ -61,10 +78,7 @@ describe('Shape rendering with DOM renderer', () => {
stroke: '#000000',
};
const shapeId = surfaceModel.addElement(shapeProps);
await wait(100);
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
const shapeElement = await waitForShapeElement(surfaceView, shapeId);
expect(shapeElement).not.toBeNull();
const zoom = surfaceView.renderer.viewport.zoom;
@@ -84,16 +98,12 @@ describe('Shape rendering with DOM renderer', () => {
};
const shapeId = surfaceModel.addElement(shapeProps);
await new Promise(resolve => setTimeout(resolve, 100));
let shapeElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${shapeId}"]`
);
let shapeElement = await waitForShapeElement(surfaceView, shapeId);
expect(shapeElement).not.toBeNull();
surfaceModel.deleteElement(shapeId);
await new Promise(resolve => setTimeout(resolve, 100));
await wait(100);
shapeElement = surfaceView.renderRoot.querySelector(
`[data-element-id="${shapeId}"]`
@@ -113,10 +123,7 @@ describe('Shape rendering with DOM renderer', () => {
filled: true,
};
const shapeId = surfaceModel.addElement(shapeProps);
await wait(100);
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
const shapeElement = await waitForShapeElement(surfaceView, shapeId);
expect(shapeElement).not.toBeNull();
const zoom = surfaceView.renderer.viewport.zoom;
@@ -136,10 +143,7 @@ describe('Shape rendering with DOM renderer', () => {
filled: true,
};
const shapeId = surfaceModel.addElement(shapeProps);
await wait(100);
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
`[data-element-id="${shapeId}"]`
);
const shapeElement = await waitForShapeElement(surfaceView, shapeId);
expect(shapeElement).not.toBeNull();
const zoom = surfaceView.renderer.viewport.zoom;
+1 -1
View File
@@ -79,7 +79,7 @@
"dependencies": {
"async-call-rpc": "^6.4.2",
"electron-updater": "^6.8.3",
"link-preview-js": "^3.1.0",
"link-preview-js": "^4.0.0",
"next-themes": "^0.4.4",
"set-cookie-parser": "^2.7.1",
"yjs": "^13.6.27"
@@ -175,7 +175,7 @@ export const uiHandlers = {
timeout: 6000,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0 Safari/537.36 Edg/120.0.0',
},
followRedirects: 'manual',
handleRedirects: (_baseUrl: string, forwardedUrl: string) => {
@@ -53,34 +53,34 @@ test.describe('AIChatWith/Attachments', () => {
loggedInPage: page,
utils,
}) => {
const randomStr1 = Math.random().toString(36).substring(2, 6);
const randomStr2 = Math.random().toString(36).substring(2, 6);
const textContent1 = `Attachment${randomStr1} is a cute cat`;
const textContent2 = `Attachment${randomStr2} is a cute dog`;
const textContent1 = 'This attachment describes a cute cat.';
const textContent2 = 'This attachment describes a cute dog.';
const buffer1 = Buffer.from(textContent1);
const buffer2 = Buffer.from(textContent2);
const firstName = 'cat-document.txt';
const secondName = 'dog-document.txt';
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: 'document1.txt',
name: firstName,
mimeType: 'text/plain',
buffer: buffer1,
},
{
name: 'document2.txt',
name: secondName,
mimeType: 'text/plain',
buffer: buffer2,
},
],
`Which animal is Attachment${randomStr1} and which animal is Attachment${randomStr2}? Answer with both attachment names.`
`Which animal is described in ${firstName} and which animal is described in ${secondName}? Answer with both attachment names.`
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `Which animal is Attachment${randomStr1} and which animal is Attachment${randomStr2}? Answer with both attachment names.`,
content: `Which animal is described in ${firstName} and which animal is described in ${secondName}? Answer with both attachment names.`,
},
{
role: 'assistant',
@@ -90,8 +90,8 @@ test.describe('AIChatWith/Attachments', () => {
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(new RegExp(`Attachment${randomStr1}`));
expect(content).toMatch(new RegExp(`Attachment${randomStr2}`));
expect(content).toContain(firstName);
expect(content).toContain(secondName);
expect(content).toMatch(/cat/i);
expect(content).toMatch(/dog/i);
}).toPass({ timeout: 20000 });
@@ -37,11 +37,11 @@ test.describe('AIChatWith/Image', () => {
},
]);
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
await expect(
content.toLowerCase().includes('kitten') ||
content.toLowerCase().includes('cat')
).toBe(true);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content.length).toBeGreaterThan(20);
expect(content).toMatch(/cat|kitten|feline|tabby|fluffy/i);
}).toPass({ timeout: 20000 });
});
test('support chat with mutiple images', async ({
@@ -28,11 +28,10 @@ test.describe('AIInsertion/SaveAsBlock', () => {
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.saveAsBlock();
await page.getByText('Successfully saved chat to a block').waitFor();
// Switch to edgeless mode
await utils.editor.isEdgelessMode(page);
// Verify the ai block is created
await page.waitForSelector('affine-edgeless-ai-chat');
const aiBlock = await page.locator('affine-edgeless-ai-chat');
await expect(aiBlock).toBeVisible();
@@ -513,9 +513,9 @@ test.describe('AISettings/Embedding', () => {
await utils.settings.openSettingsPanel(page);
await page.context().setOffline(true);
await utils.settings.ignoreDocForEmbedding(page, 'Test Doc', false);
await expect(
page.getByText(/Failed to update ignored docs/i)
).toBeVisible();
await page
.getByText(/Failed to update ignored docs/i)
.waitFor({ state: 'visible', timeout: 20000 });
await page.context().setOffline(false);
});
});
@@ -575,7 +575,14 @@ export class EditorUtils {
),
generateImage: this.createAction(page, async () => {
await page.getByTestId('action-generate-image').click();
await page.keyboard.type('generate an image');
const input = page.locator(
'affine-ai-panel-widget .ai-panel-container textarea'
);
await input.waitFor({ state: 'visible' });
await input.fill('generate an image');
await page.getByTestId('ai-panel-input-send').waitFor({
state: 'visible',
});
await page.getByTestId('ai-panel-input-send').click();
}),
generateCaption: this.createAction(page, () =>
@@ -6,6 +6,9 @@ const WORKSPACE_EMBEDDING_SWITCH_TEST_ID = 'workspace-embedding-setting-switch';
export class SettingsPanelUtils {
public static async openSettingsPanel(page: Page) {
if (await page.getByTestId('workspace-setting:embedding').isHidden()) {
await page
.getByTestId('slider-bar-workspace-setting-button')
.waitFor({ state: 'visible' });
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:embedding').click();
await page.getByTestId('workspace-embedding-setting-header').waitFor({
@@ -18,7 +21,7 @@ export class SettingsPanelUtils {
if (
await page.getByTestId('workspace-embedding-setting-wrapper').isVisible()
) {
await page.getByTestId('modal-close-button').click();
await page.keyboard.press('Escape');
await page.getByTestId('workspace-embedding-setting-wrapper').waitFor({
state: 'hidden',
});
@@ -161,13 +164,22 @@ export class SettingsPanelUtils {
const searcher = await page.getByTestId('doc-selector-layout');
const searchInput = await page.getByTestId('doc-selector-search-input');
await searchInput.waitFor({ state: 'visible' });
await searchInput.focus();
await page.keyboard.insertText(doc);
const pageListItem = searcher.getByTestId('doc-list-item');
await expect(pageListItem).toHaveCount(1);
const pageListItemTitle = pageListItem.getByTestId('doc-list-item-title');
await expect(pageListItemTitle).toHaveText(doc);
const pageListItem = searcher
.getByTestId('doc-list-item')
.filter({
has: page
.getByTestId('doc-list-item-title')
.getByText(doc, { exact: true }),
})
.first();
await expect(pageListItem).toBeVisible();
await expect(pageListItem.getByTestId('doc-list-item-title')).toHaveText(
doc
);
await pageListItem.click();
await searcher.getByTestId('doc-selector-confirm-button').click();
@@ -200,7 +212,7 @@ export class SettingsPanelUtils {
await searchInput.focus();
await page.keyboard.insertText(doc);
const pageListItem = searcher.getByTestId('page-list-item');
const pageListItem = searcher.getByTestId('doc-list-item');
await expect(pageListItem).toHaveCount(1);
await pageListItem.getByTestId('affine-checkbox').uncheck();
+6 -3
View File
@@ -37,9 +37,12 @@ export async function waitForAllPagesLoad(page: Page) {
}
export async function clickNewPageButton(page: Page, title?: string) {
await page.getByTestId('sidebar-new-page-button').click({
// default timeout is 5000ms, but it's not enough for the CI first page load
timeout: 20000,
const newPageButton = page.getByTestId('sidebar-new-page-button');
await expect(newPageButton).toBeVisible({
timeout: 30000,
});
await newPageButton.click({
timeout: 30000,
});
await waitForEmptyEditor(page);
if (title) {
+1 -1
View File
@@ -28,7 +28,7 @@
"clipanion": "^3.2.1",
"css-loader": "^7.1.2",
"cssnano": "^7.0.6",
"html-webpack-plugin": "^5.6.3",
"html-webpack-plugin": "^5.6.6",
"inquirer": "^12.3.0",
"jsonc-parser": "^3.3.1",
"lodash-es": "^4.17.23",
+56 -59
View File
@@ -131,7 +131,7 @@ __metadata:
clipanion: "npm:^3.2.1"
css-loader: "npm:^7.1.2"
cssnano: "npm:^7.0.6"
html-webpack-plugin: "npm:^5.6.3"
html-webpack-plugin: "npm:^5.6.6"
inquirer: "npm:^12.3.0"
jsonc-parser: "npm:^3.3.1"
lodash-es: "npm:^4.17.23"
@@ -607,7 +607,7 @@ __metadata:
esbuild: "npm:^0.25.0"
fs-extra: "npm:^11.2.0"
glob: "npm:^11.0.0"
link-preview-js: "npm:^3.1.0"
link-preview-js: "npm:^4.0.0"
lodash-es: "npm:^4.17.23"
msw: "npm:^2.12.4"
nanoid: "npm:^5.1.6"
@@ -16863,7 +16863,16 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:>=8.1.0, @types/node@npm:^22.0.0, @types/node@npm:^22.5.5, @types/node@npm:^22.7.7":
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:>=20.0.0, @types/node@npm:>=8.1.0":
version: 25.5.0
resolution: "@types/node@npm:25.5.0"
dependencies:
undici-types: "npm:~7.18.0"
checksum: 10/b1e8116bd8c9ff62e458b76d28a59cf7631537bb17e8961464bf754dd5b07b46f1620f568b2f89970505af9eef478dd74c614651b454c1ea95949ec472c64fcb
languageName: node
linkType: hard
"@types/node@npm:^22.0.0, @types/node@npm:^22.5.5, @types/node@npm:^22.7.7":
version: 22.19.1
resolution: "@types/node@npm:22.19.1"
dependencies:
@@ -16872,15 +16881,6 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^20.0.0":
version: 20.19.1
resolution: "@types/node@npm:20.19.1"
dependencies:
undici-types: "npm:~6.21.0"
checksum: 10/2369a96a832a6e8111d2b996c56fb1b78b27fad0879de3e344ae2835697464e001d6fe813a11902348fc7dc63b9510000f3d2cd4bedadf2d3fce7b7bb7903444
languageName: node
linkType: hard
"@types/nodemailer@npm:^7.0.0":
version: 7.0.9
resolution: "@types/nodemailer@npm:7.0.9"
@@ -22464,6 +22464,13 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^7.0.1":
version: 7.0.1
resolution: "entities@npm:7.0.1"
checksum: 10/3c0c58d869c45148463e96d21dee2d1b801bd3fe4cf47aa470cd26dfe81d59e9e0a9be92ae083fa02fa441283c883a471486e94538dcfb8544428aa80a55271b
languageName: node
linkType: hard
"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -24763,15 +24770,16 @@ __metadata:
linkType: hard
"happy-dom@npm:^20.0.0, happy-dom@npm:^20.3.0":
version: 20.3.0
resolution: "happy-dom@npm:20.3.0"
version: 20.8.9
resolution: "happy-dom@npm:20.8.9"
dependencies:
"@types/node": "npm:^20.0.0"
"@types/node": "npm:>=20.0.0"
"@types/whatwg-mimetype": "npm:^3.0.2"
"@types/ws": "npm:^8.18.1"
entities: "npm:^7.0.1"
whatwg-mimetype: "npm:^3.0.0"
ws: "npm:^8.18.3"
checksum: 10/aade5560110eeaad502679a314f4d3b8658fe4697c1914d0de09ec6a176611ad0c8953f0aa559c56d1afabe0ab8465cde7b4811006df6eb3b54c5e5ea4169b29
checksum: 10/81fabd8c630c2165eedd57b72f38b58e8891ff2ff543302c5bdf88860fc3b45b16decd27dabfc72739af6a927d711b04008c61b7e8090893da1fe5feba2dc80e
languageName: node
linkType: hard
@@ -25088,9 +25096,9 @@ __metadata:
languageName: node
linkType: hard
"html-webpack-plugin@npm:^5.6.3":
version: 5.6.3
resolution: "html-webpack-plugin@npm:5.6.3"
"html-webpack-plugin@npm:^5.6.6":
version: 5.6.6
resolution: "html-webpack-plugin@npm:5.6.6"
dependencies:
"@types/html-minifier-terser": "npm:^6.0.0"
html-minifier-terser: "npm:^6.0.2"
@@ -25105,7 +25113,7 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/fd2bf1ac04823526c8b609555d027b38b9d61b4ba9f5c8116a37cc6b62d5b86cab1f478616e8c5344fee13663d2566f5c470c66265ecb1e9574dc38d0459889d
checksum: 10/819ebee079466029a771236fdadcbcfe0aaf110eac1b74c0983a0318a7f99f3c69adcf1d617e218769a8c8e96ea17de2df30759bd950ec391d0c5676d480894e
languageName: node
linkType: hard
@@ -26950,13 +26958,12 @@ __metadata:
languageName: node
linkType: hard
"link-preview-js@npm:^3.1.0":
version: 3.1.0
resolution: "link-preview-js@npm:3.1.0"
"link-preview-js@npm:^4.0.0":
version: 4.0.0
resolution: "link-preview-js@npm:4.0.0"
dependencies:
cheerio: "npm:1.0.0-rc.11"
url: "npm:0.11.0"
checksum: 10/35d2891e3b9895ca2f54834be9c6d0c52d90c9f16124a45acac259dde4b960f3069aadf46bf6e087a1df6574762bbfc49bdf3e795fb29863feb9cb94be39c7be
checksum: 10/6a31e37cdc2d114bf425c4aa7243f58ee1e473842a7874f16a05fd5afbaf0792c15071ba8ae31ce845b1ef1c54dc8c3495fdfe9fa8375fcf3725216f24f6d68c
languageName: node
linkType: hard
@@ -29260,9 +29267,9 @@ __metadata:
linkType: hard
"node-forge@npm:^1":
version: 1.3.3
resolution: "node-forge@npm:1.3.3"
checksum: 10/f41c31b9296771a4b8c955d58417471712f54f324603a35f8e6cbac19d5e6eaaf5fd5fd14584dfedecbf46a05438ded6eee60a5f2f0822fc5061aaa073cfc75d
version: 1.4.0
resolution: "node-forge@npm:1.4.0"
checksum: 10/d70fd769768e646eda73343d4d4105ccb6869315d975905a22117431c04ae5b6df6c488e34ed275b1a66b50195a09b84b5c8aeca3b8605c20605fcb8e9f109d9
languageName: node
linkType: hard
@@ -29356,9 +29363,9 @@ __metadata:
linkType: hard
"nodemailer@npm:^7.0.11":
version: 7.0.11
resolution: "nodemailer@npm:7.0.11"
checksum: 10/2ad4dd56a4caf84a83aa6f4378ded26d5ef8a644ca3be09c3b4fb2255d861369e620f29be6c3c97148ac4a50aa5fdff6240b9d60805362bd99ca15f2ea62e8a2
version: 7.0.13
resolution: "nodemailer@npm:7.0.13"
checksum: 10/4ae37bfb6896f1555de150c1098a7c1b28eb1da240a4c7adede81a8af0dcc184298cfbf804930599c076ea574428e0c18c6b28f0dc2072cebb30c1da5fd0b688
languageName: node
linkType: hard
@@ -30365,7 +30372,7 @@ __metadata:
languageName: node
linkType: hard
"path-to-regexp@npm:8.3.0, path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.3.0":
"path-to-regexp@npm:8.3.0":
version: 8.3.0
resolution: "path-to-regexp@npm:8.3.0"
checksum: 10/568f148fc64f5fd1ecebf44d531383b28df924214eabf5f2570dce9587a228e36c37882805ff02d71c6209b080ea3ee6a4d2b712b5df09741b67f1f3cf91e55a
@@ -30379,10 +30386,17 @@ __metadata:
languageName: node
linkType: hard
"path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.3.0":
version: 8.4.0
resolution: "path-to-regexp@npm:8.4.0"
checksum: 10/6864561ceacaece330a2213c6eb21505519673a65b4249e0e5d518984528f93b302b8bf85ccafc4f9d7f359f919d8b118dc00fdb0b26ded3d21e8802cffdfcb8
languageName: node
linkType: hard
"path-to-regexp@npm:~0.1.12":
version: 0.1.12
resolution: "path-to-regexp@npm:0.1.12"
checksum: 10/2e30f6a0144679c1f95c98e166b96e6acd1e72be9417830fefc8de7ac1992147eb9a4c7acaa59119fb1b3c34eec393b2129ef27e24b2054a3906fc4fb0d1398e
version: 0.1.13
resolution: "path-to-regexp@npm:0.1.13"
checksum: 10/f1e4bdedc4fd41a3b8dd76e8b2e1183105348c6b205badc072581ca63dc6aa7976a8a67feaffcf0e505f51ac12cb1a2de7f3fef3e9085b6849e76232d73ddcba
languageName: node
linkType: hard
@@ -31349,13 +31363,6 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:1.3.2":
version: 1.3.2
resolution: "punycode@npm:1.3.2"
checksum: 10/5c57d588c60679fd1b9400c75de06e327723f2b38e21e195027ba7a59006725f7b817dce5b26d47c7f8c1c842d28275aa59955a06d2e467cffeba70b7e0576bb
languageName: node
linkType: hard
"punycode@npm:^2.1.0":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@@ -31390,13 +31397,6 @@ __metadata:
languageName: node
linkType: hard
"querystring@npm:0.2.0":
version: 0.2.0
resolution: "querystring@npm:0.2.0"
checksum: 10/37b91720be8c8de87b49d1a68f0ceafbbeda6efe6334ce7aad080b0b4111f933a40650b8a6669c1bc629cd8bb37c67cb7b5a42ec0758662efbce44b8faa1766d
languageName: node
linkType: hard
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -35183,6 +35183,13 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~7.18.0":
version: 7.18.2
resolution: "undici-types@npm:7.18.2"
checksum: 10/e61a5918f624d68420c3ca9d301e9f15b61cba6e97be39fe2ce266dd6151e4afe424d679372638826cb506be33952774e0424141200111a9857e464216c009af
languageName: node
linkType: hard
"unicode-properties@npm:^1.2.2":
version: 1.4.1
resolution: "unicode-properties@npm:1.4.1"
@@ -35541,16 +35548,6 @@ __metadata:
languageName: node
linkType: hard
"url@npm:0.11.0":
version: 0.11.0
resolution: "url@npm:0.11.0"
dependencies:
punycode: "npm:1.3.2"
querystring: "npm:0.2.0"
checksum: 10/beec744c7ade6ef178fd631e2fe70110c5c53f9e7caea5852703214bfcbf03fd136b98b3b6f4a08bd2420a76f569cbc10c2a86ade7f836ac7d9ff27ed62d8d2d
languageName: node
linkType: hard
"urlpattern-polyfill@npm:^10.0.0":
version: 10.1.0
resolution: "urlpattern-polyfill@npm:10.1.0"