Skip to content

Commit 530bdf6

Browse files
committed
Merge remote-tracking branch 'origin/main' into release/v1.4
2 parents 6084f79 + d231320 commit 530bdf6

5 files changed

Lines changed: 82 additions & 44 deletions

File tree

.husky/pre-push

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/sh
2+
3+
# Skip checks: SKIP_PRE_PUSH=1 git push or git push --no-verify
4+
if [ "$SKIP_PRE_PUSH" = "1" ]; then
5+
echo "⏭ SKIP_PRE_PUSH=1, skipping pre-push checks"
6+
exit 0
7+
fi
8+
9+
# Only run checks when pushing to main or release/* branches
10+
remote="$1"
11+
need_check=0
12+
13+
while read local_ref local_sha remote_ref remote_sha; do
14+
branch=$(echo "$remote_ref" | sed 's|refs/heads/||')
15+
16+
if [ "$branch" = "main" ] || echo "$branch" | grep -q "^release"; then
17+
need_check=1
18+
echo "🔍 Detected push target: $branch"
19+
fi
20+
done
21+
22+
if [ "$need_check" = "0" ]; then
23+
exit 0
24+
fi
25+
26+
echo ""
27+
echo "▶ Running lint..."
28+
pnpm run lint || exit 1
29+
30+
echo ""
31+
echo "▶ Running tests..."
32+
pnpm run test:ci || exit 1
33+
34+
echo ""
35+
echo "✅ All checks passed! (build and e2e tests will run in CI)"

e2e/gm-api.spec.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ const test = base.extend<{
2020
args: ["--headless=new", ...chromeArgs],
2121
});
2222
let [bg] = ctx1.serviceWorkers();
23-
if (!bg) bg = await ctx1.waitForEvent("serviceworker");
23+
if (!bg) bg = await ctx1.waitForEvent("serviceworker", { timeout: 30_000 });
2424
const extensionId = bg.url().split("/")[2];
2525
const extPage = await ctx1.newPage();
2626
await extPage.goto("chrome://extensions/");
2727
await extPage.waitForLoadState("domcontentloaded");
28-
await extPage.waitForTimeout(1_000);
28+
// Wait for developerPrivate API to be available instead of a fixed delay
29+
await extPage.waitForFunction(() => !!(chrome as any).developerPrivate, { timeout: 10_000 });
2930
await extPage.evaluate(async (id) => {
3031
await (chrome as any).developerPrivate.updateExtensionConfiguration({
3132
extensionId: id,
@@ -40,6 +41,10 @@ const test = base.extend<{
4041
headless: false,
4142
args: ["--headless=new", ...chromeArgs],
4243
});
44+
// Ensure service worker is registered before handing context to fixtures,
45+
// preventing extensionId fixture from timing out with the global 10s timeout.
46+
const [sw] = context.serviceWorkers();
47+
if (!sw) await context.waitForEvent("serviceworker", { timeout: 30_000 });
4348
await use(context);
4449
await context.close();
4550
fs.rmSync(userDataDir, { recursive: true, force: true });
@@ -117,24 +122,25 @@ async function runTestScript(
117122

118123
const page = await context.newPage();
119124
const logs: string[] = [];
120-
page.on("console", (msg) => logs.push(msg.text()));
121-
122-
await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
123-
124-
// Wait for test results to appear in console
125-
const deadline = Date.now() + timeoutMs;
126125
let passed = -1;
127126
let failed = -1;
128-
while (Date.now() < deadline) {
129-
for (const log of logs) {
130-
const passMatch = log.match(/[:]\s*(\d+)/);
131-
const failMatch = log.match(/[:]\s*(\d+)/);
127+
128+
// Resolve as soon as both pass and fail counts appear in console output
129+
const resultReady = new Promise<void>((resolve) => {
130+
page.on("console", (msg) => {
131+
const text = msg.text();
132+
logs.push(text);
133+
const passMatch = text.match(/[:]\s*(\d+)/);
134+
const failMatch = text.match(/[:]\s*(\d+)/);
132135
if (passMatch) passed = parseInt(passMatch[1], 10);
133136
if (failMatch) failed = parseInt(failMatch[1], 10);
134-
}
135-
if (passed >= 0 && failed >= 0) break;
136-
await page.waitForTimeout(500);
137-
}
137+
if (passed >= 0 && failed >= 0) resolve();
138+
});
139+
});
140+
141+
await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
142+
// Race: resolve immediately when results arrive, or fall through after timeout
143+
await Promise.race([resultReady, page.waitForTimeout(timeoutMs)]);
138144

139145
await page.close();
140146
return { passed, failed, logs };

e2e/utils.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,22 @@ export async function openEditorPage(context: BrowserContext, extensionId: strin
3636
/** Install a script by injecting code into the Monaco editor and saving */
3737
export async function installScriptByCode(context: BrowserContext, extensionId: string, code: string): Promise<void> {
3838
const page = await openEditorPage(context, extensionId);
39-
// Wait for Monaco editor to be ready
39+
// Wait for Monaco editor DOM and default template content to be ready
4040
await page.locator(".monaco-editor").waitFor({ timeout: 30_000 });
4141
await page.locator(".view-lines").waitFor({ timeout: 15_000 });
42-
// Click into editor to ensure focus
42+
// Click to focus and wait for the cursor to appear (confirms editor is interactive)
4343
await page.locator(".monaco-editor .view-lines").click();
44-
await page.waitForTimeout(500);
45-
// Select all existing content and replace via clipboard
44+
await page.locator(".cursors-layer .cursor").waitFor({ timeout: 5_000 });
45+
// Select all existing content
4646
await page.keyboard.press("ControlOrMeta+a");
47-
await page.waitForTimeout(500);
47+
// Capture current content fingerprint, then paste replacement
48+
const initialText = await page.locator(".view-lines").textContent();
4849
await page.evaluate((text) => navigator.clipboard.writeText(text), code);
4950
await page.keyboard.press("ControlOrMeta+v");
50-
await page.waitForTimeout(2000);
51+
// Wait for Monaco to finish rendering the pasted content (content will differ from template)
52+
await page.waitForFunction((init) => document.querySelector(".view-lines")?.textContent !== init, initialText, {
53+
timeout: 10_000,
54+
});
5155
// Save
5256
await page.keyboard.press("ControlOrMeta+s");
5357
// Wait for save: try arco-message first, then verify via script list
@@ -61,13 +65,9 @@ export async function installScriptByCode(context: BrowserContext, extensionId:
6165
// For scripts with @require/@resource, the message may not appear.
6266
// Verify save by checking the script list on the options page.
6367
const listPage = await openOptionsPage(context, extensionId);
64-
await listPage.waitForTimeout(2_000);
6568
const emptyState = listPage.locator(".arco-empty");
66-
// Wait until at least one script appears (no empty state)
67-
for (let i = 0; i < 30; i++) {
68-
if ((await emptyState.count()) === 0) break;
69-
await listPage.waitForTimeout(1_000);
70-
}
69+
// Wait until at least one script appears (no empty state), up to 30s
70+
await emptyState.waitFor({ state: "detached", timeout: 30_000 }).catch(() => {});
7171
await listPage.close();
7272
}
7373
await page.close();

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"private": true,
88
"scripts": {
99
"preinstall": "pnpm dlx only-allow pnpm",
10+
"prepare": "husky",
1011
"test": "vitest --test-timeout=500 --no-coverage --isolate=false --reporter=verbose",
1112
"test:ci": "vitest run --test-timeout=500 --no-coverage --isolate=false --reporter=default --reporter.summary=false",
1213
"coverage": "vitest run --coverage",
@@ -84,6 +85,7 @@
8485
"eslint-plugin-react-hooks": "^5.2.0",
8586
"eslint-plugin-userscripts": "^0.5.6",
8687
"globals": "^16.5.0",
88+
"husky": "^9.1.7",
8789
"iconv-lite": "^0.7.2",
8890
"jsdom": "^26.1.0",
8991
"jszip": "^3.10.1",
@@ -104,4 +106,4 @@
104106
"**/*.scss",
105107
"**/*.less"
106108
]
107-
}
109+
}

pnpm-lock.yaml

Lines changed: 10 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)