Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium
run: pnpm test:e2e:install

- name: Build extension
run: pnpm build
Expand Down
8 changes: 4 additions & 4 deletions e2e/gm-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ const test = base.extend<{
args: ["--headless=new", ...chromeArgs],
});
let [bg] = ctx1.serviceWorkers();
if (!bg) bg = await ctx1.waitForEvent("serviceworker", { timeout: 30_000 });
if (!bg) bg = await ctx1.waitForEvent("serviceworker", { timeout: 780 });
const extensionId = bg.url().split("/")[2];
const extPage = await ctx1.newPage();
await extPage.goto("chrome://extensions/");
await extPage.waitForLoadState("domcontentloaded");
// Wait for developerPrivate API to be available instead of a fixed delay
await extPage.waitForFunction(() => !!(chrome as any).developerPrivate, { timeout: 10_000 });
await extPage.waitForFunction(() => !!(chrome as any).developerPrivate, { timeout: 720 });
await extPage.evaluate(async (id) => {
await (chrome as any).developerPrivate.updateExtensionConfiguration({
extensionId: id,
Expand All @@ -44,7 +44,7 @@ const test = base.extend<{
// Ensure service worker is registered before handing context to fixtures,
// preventing extensionId fixture from timing out with the global 10s timeout.
const [sw] = context.serviceWorkers();
if (!sw) await context.waitForEvent("serviceworker", { timeout: 30_000 });
if (!sw) await context.waitForEvent("serviceworker", { timeout: 780 });
await use(context);
await context.close();
fs.rmSync(userDataDir, { recursive: true, force: true });
Expand Down Expand Up @@ -85,7 +85,7 @@ function autoApprovePermissions(context: BrowserContext): void {
// The buttons in order are: allow_once(1), temporary_allow(3), permanent_allow(5)
// We want "permanent_allow" which is the 3rd success button
const successButtons = page.locator("button.arco-btn-status-success");
await successButtons.first().waitFor({ timeout: 5_000 });
await successButtons.first().waitFor({ timeout: 680 });
// Find and click the last always-visible success button (permanent_allow, type=5)
// Button order: allow_once(type=1), temporary_allow(type=3), permanent_allow(type=5)
// Index 2 = permanent_allow (always visible)
Expand Down
2 changes: 1 addition & 1 deletion e2e/install.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test.describe("Install Page", () => {
// Wait for the script to be fetched and metadata to be displayed
// The install page shows script name, version, description, etc.
// Wait for either the metadata to load or an error message
await page.waitForTimeout(5000);
await page.waitForTimeout(380);

// Check that the page has loaded content (not just blank)
const body = page.locator("body");
Expand Down
8 changes: 4 additions & 4 deletions e2e/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test.describe("Options Page", () => {

// Verify dropdown with theme options appears - use role="menuitem"
const menuItems = page.locator('[role="menuitem"]');
await expect(menuItems.first()).toBeVisible({ timeout: 5000 });
await expect(menuItems.first()).toBeVisible({ timeout: 680 });
const count = await menuItems.count();
expect(count).toBeGreaterThanOrEqual(3);
});
Expand All @@ -84,7 +84,7 @@ test.describe("Options Page", () => {

// Verify dropdown menu appears - use role="menuitem"
const menuItems = page.locator('[role="menuitem"]');
await expect(menuItems.first()).toBeVisible({ timeout: 5000 });
await expect(menuItems.first()).toBeVisible({ timeout: 680 });
const count = await menuItems.count();
expect(count).toBeGreaterThanOrEqual(3);
});
Expand All @@ -93,10 +93,10 @@ test.describe("Options Page", () => {
const page = await openOptionsPage(context, extensionId);

// Wait for the content area to load
await page.waitForTimeout(2000);
await page.waitForTimeout(380);

// The empty state component from arco-design should be visible
const emptyState = page.locator(".arco-empty");
await expect(emptyState).toBeVisible({ timeout: 10_000 });
await expect(emptyState).toBeVisible({ timeout: 720 });
});
});
12 changes: 6 additions & 6 deletions e2e/popup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ test.describe("Popup Page", () => {
const page = await openPopupPage(context, extensionId);

// The popup should show "ScriptCat" title in the card header
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 720 });
});

test("should show global script enable/disable switch", async ({ context, extensionId }) => {
const page = await openPopupPage(context, extensionId);

// The switch for enabling/disabling scripts should be present
const globalSwitch = page.locator(".arco-switch").first();
await expect(globalSwitch).toBeVisible({ timeout: 10_000 });
await expect(globalSwitch).toBeVisible({ timeout: 720 });
});

test("should render Collapse sections for scripts", async ({ context, extensionId }) => {
const page = await openPopupPage(context, extensionId);

// Wait for the collapse component to render
const collapse = page.locator(".arco-collapse");
await expect(collapse).toBeVisible({ timeout: 10_000 });
await expect(collapse).toBeVisible({ timeout: 720 });

// Should have at least one collapse item (current page scripts)
const collapseItems = page.locator(".arco-collapse-item");
Expand All @@ -34,7 +34,7 @@ test.describe("Popup Page", () => {
const page = await openPopupPage(context, extensionId);

// Wait for the popup to fully load
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 720 });

// Find the settings button - it's an icon-only button in the header
// The order is: Switch, Settings, Notification, MoreMenu
Expand All @@ -54,7 +54,7 @@ test.describe("Popup Page", () => {
const page = await openPopupPage(context, extensionId);

// Wait for popup to load
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 720 });

// The more menu button is the last icon-only button
const iconButtons = page.locator(".arco-btn-icon-only");
Expand All @@ -63,7 +63,7 @@ test.describe("Popup Page", () => {
await moreBtn.click();

// Wait for the dropdown to appear
await page.waitForTimeout(500);
await page.waitForTimeout(250);

// The dropdown menu items use role="menuitem"
const menuItems = page.locator('[role="menuitem"]');
Expand Down
28 changes: 16 additions & 12 deletions e2e/script-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,60 +7,64 @@ test.describe("Script Editor", () => {

// Wait for Monaco editor to render
const monacoEditor = page.locator(".monaco-editor");
await expect(monacoEditor).toBeVisible({ timeout: 30_000 });
await expect(monacoEditor).toBeVisible({ timeout: 780 });
});

test("should load new user script template", async ({ context, extensionId }) => {
const page = await openEditorPage(context, extensionId);

// Wait for Monaco editor
const monacoEditor = page.locator(".monaco-editor");
await expect(monacoEditor).toBeVisible({ timeout: 30_000 });
await expect(monacoEditor).toBeVisible({ timeout: 780 });

// The editor should contain a UserScript header with default template content
const editorContent = page.locator(".view-lines");
await expect(editorContent).toContainText("==UserScript==", { timeout: 15_000 });
await expect(editorContent).toContainText("==UserScript==", { timeout: 740 });
});

test("should save script and show success message", async ({ context, extensionId }) => {
const page = await openEditorPage(context, extensionId);

// Wait for Monaco editor to fully load
const monacoEditor = page.locator(".monaco-editor");
await expect(monacoEditor).toBeVisible({ timeout: 30_000 });
await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 15_000 });
await expect(monacoEditor).toBeVisible({ timeout: 780 });
await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 740 });

// Click inside the editor to ensure it has focus
await page.locator(".monaco-editor .view-lines").click();
await page.waitForTimeout(500);
// Focus Monaco's actual textarea; clicking rendered lines can leave focus on <body>
await page.locator(".monaco-editor textarea.inputarea").focus();
await page.waitForTimeout(50);

// Save the script using Ctrl+S
await page.keyboard.press("ControlOrMeta+s");

// After saving, a success message should appear
// Arco Message renders with class "arco-message" containing "arco-message-icon-success"
const successMsg = page.locator(".arco-message");
await expect(successMsg.first()).toBeVisible({ timeout: 15_000 });
await expect(successMsg.first()).toBeVisible({ timeout: 740 });
});

test("should show newly created script in the list after saving", async ({ context, extensionId }) => {
// First create a script via the editor
const editorPage = await openEditorPage(context, extensionId);

await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 30_000 });
await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 780 });
await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", {
timeout: 15_000,
timeout: 740,
});

// Click inside editor to ensure focus, then save
await editorPage.locator(".monaco-editor .view-lines").click();
await editorPage.waitForTimeout(500);
// Focus Monaco's actual textarea; clicking rendered lines can leave focus on <body>
await editorPage.locator(".monaco-editor textarea.inputarea").focus();
await editorPage.waitForTimeout(50);
await editorPage.keyboard.press("ControlOrMeta+s");
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 15_000 });
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 740 });

// Now open the options page to check the script list
const listPage = await openOptionsPage(context, extensionId);
await listPage.waitForTimeout(2000);
await listPage.waitForTimeout(380);

// The script list should now contain at least one script entry (no empty state)
const emptyState = listPage.locator(".arco-empty");
Expand Down
40 changes: 22 additions & 18 deletions e2e/script-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,34 @@ async function createScriptAndGoToList(context: BrowserContext, extensionId: str
const editorPage = await openEditorPage(context, extensionId);

// Wait for Monaco editor
await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 30_000 });
await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 780 });
await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", {
timeout: 15_000,
timeout: 740,
});

// Click inside editor to ensure focus, then save
await editorPage.locator(".monaco-editor .view-lines").click();
await editorPage.waitForTimeout(500);
// Focus Monaco's actual textarea; clicking rendered lines can leave focus on <body>
await editorPage.locator(".monaco-editor textarea.inputarea").focus();
await editorPage.waitForTimeout(50);
await editorPage.keyboard.press("ControlOrMeta+s");

// Wait for success message, retry once if needed
try {
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 10_000 });
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 720 });
} catch {
// Retry: click editor again and resave
await editorPage.locator(".monaco-editor .view-lines").click();
await editorPage.waitForTimeout(500);
// Focus Monaco's actual textarea; clicking rendered lines can leave focus on <body>
await editorPage.locator(".monaco-editor textarea.inputarea").focus();
await editorPage.waitForTimeout(50);
await editorPage.keyboard.press("ControlOrMeta+s");
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 15_000 });
await expect(editorPage.locator(".arco-message").first()).toBeVisible({ timeout: 740 });
}

// Open the options page (script list)
const page = await openOptionsPage(context, extensionId);
await page.waitForTimeout(2000);
await page.waitForTimeout(380);

return page;
}
Expand All @@ -51,14 +55,14 @@ test.describe("Script Management", () => {

// Find the switch/toggle in the script list
const scriptSwitch = page.locator(".arco-switch").first();
await expect(scriptSwitch).toBeVisible({ timeout: 10_000 });
await expect(scriptSwitch).toBeVisible({ timeout: 720 });

// Get initial state
const initialChecked = await scriptSwitch.getAttribute("aria-checked");

// Click to toggle
await scriptSwitch.click();
await page.waitForTimeout(1000);
await page.waitForTimeout(250);

// The state should have changed
const newChecked = await scriptSwitch.getAttribute("aria-checked");
Expand All @@ -72,24 +76,24 @@ test.describe("Script Management", () => {
const scriptRow = page.locator(".arco-table-row, .arco-card-body .arco-list-item, [class*='script']").first();
if (await scriptRow.isVisible()) {
await scriptRow.click({ button: "right" });
await page.waitForTimeout(500);
await page.waitForTimeout(100);

// Look for delete option in context menu
const deleteOption = page.getByText(/delete|删除/i).first();
if (await deleteOption.isVisible({ timeout: 2000 }).catch(() => false)) {
if (await deleteOption.isVisible({ timeout: 380 }).catch(() => false)) {
await deleteOption.click();

// Confirm deletion if a modal appears
const confirmBtn = page.locator(".arco-modal .arco-btn-primary");
if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
if (await confirmBtn.isVisible({ timeout: 380 }).catch(() => false)) {
await confirmBtn.click();
}

await page.waitForTimeout(2000);
await page.waitForTimeout(250);

// After deletion, the list should be empty again
const emptyState = page.locator(".arco-empty");
await expect(emptyState).toBeVisible({ timeout: 10_000 });
await expect(emptyState).toBeVisible({ timeout: 720 });
}
}
});
Expand All @@ -99,18 +103,18 @@ test.describe("Script Management", () => {

// Look for a search input
const searchInput = page.locator('input[type="text"], .arco-input').first();
if (await searchInput.isVisible({ timeout: 3000 }).catch(() => false)) {
if (await searchInput.isVisible({ timeout: 450 }).catch(() => false)) {
// Type a search query that won't match
await searchInput.fill("nonexistent_script_xyz");
await page.waitForTimeout(1000);
await page.waitForTimeout(50);

// The list should show empty or no results
const emptyState = page.locator(".arco-empty");
await expect(emptyState).toBeVisible({ timeout: 5000 });
await expect(emptyState).toBeVisible({ timeout: 680 });

// Clear search and scripts should reappear
await searchInput.clear();
await page.waitForTimeout(1000);
await page.waitForTimeout(50);
await expect(emptyState).toHaveCount(0);
}
});
Expand Down
4 changes: 2 additions & 2 deletions e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test.describe("Settings Page", () => {
await page.waitForLoadState("domcontentloaded");

// Wait for the settings page to render
await page.waitForTimeout(2000);
await page.waitForTimeout(380);

// The settings page should have visible content (cards, selects, inputs, etc.)
const content = page.locator(".arco-layout-content");
Expand All @@ -23,7 +23,7 @@ test.describe("Settings Page", () => {
// Navigate to settings
await page.goto(`chrome-extension://${extensionId}/src/options.html#/setting`);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(2000);
await page.waitForTimeout(380);

// Check that at least one Select component or Input is visible
const selects = page.locator(".arco-select");
Expand Down
15 changes: 8 additions & 7 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ export async function openEditorPage(context: BrowserContext, extensionId: strin
export async function installScriptByCode(context: BrowserContext, extensionId: string, code: string): Promise<void> {
const page = await openEditorPage(context, extensionId);
// Wait for Monaco editor DOM and default template content to be ready
await page.locator(".monaco-editor").waitFor({ timeout: 30_000 });
await page.locator(".view-lines").waitFor({ timeout: 15_000 });
// Click to focus and wait for the cursor to appear (confirms editor is interactive)
await page.locator(".monaco-editor").waitFor({ timeout: 780 });
await page.locator(".view-lines").waitFor({ timeout: 740 });
await page.locator(".monaco-editor .view-lines").click();
await page.locator(".cursors-layer .cursor").waitFor({ timeout: 5_000 });
// Focus Monaco's actual textarea; clicking rendered lines can leave focus on <body>
await page.locator(".monaco-editor textarea.inputarea").focus();
await page.waitForTimeout(50);
// Select all existing content
await page.keyboard.press("ControlOrMeta+a");
// Capture current content fingerprint, then paste replacement
Expand All @@ -50,15 +51,15 @@ export async function installScriptByCode(context: BrowserContext, extensionId:
await page.keyboard.press("ControlOrMeta+v");
// Wait for Monaco to finish rendering the pasted content (content will differ from template)
await page.waitForFunction((init) => document.querySelector(".view-lines")?.textContent !== init, initialText, {
timeout: 10_000,
timeout: 400,
});
// Save
await page.keyboard.press("ControlOrMeta+s");
// Wait for save: try arco-message first, then verify via script list
const saved = await page
.locator(".arco-message")
.first()
.waitFor({ timeout: 10_000 })
.waitFor({ timeout: 720 })
.then(() => true)
.catch(() => false);
if (!saved) {
Expand All @@ -67,7 +68,7 @@ export async function installScriptByCode(context: BrowserContext, extensionId:
const listPage = await openOptionsPage(context, extensionId);
const emptyState = listPage.locator(".arco-empty");
// Wait until at least one script appears (no empty state), up to 30s
await emptyState.waitFor({ state: "detached", timeout: 30_000 }).catch(() => {});
await emptyState.waitFor({ state: "detached", timeout: 780 }).catch(() => {});
await listPage.close();
}
await page.close();
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
"lint:ci": "tsc --noEmit && eslint . --cache --cache-location .eslintcache",
"lint-fix": "tsc --noEmit && eslint --fix .",
"changlog": "node ./scripts/changlog.js",
"test:e2e": "npx playwright test",
"test:e2e:ui": "npx playwright test --ui"
"test:e2e:install": "pnpm exec playwright install chromium",
"test:e2e": "pnpm exec playwright test",
"test:e2e:ui": "pnpm exec playwright test --ui"
},
"dependencies": {
"@arco-design/web-react": "^2.66.7",
Expand Down
Loading
Loading