Skip to content

Commit 2f6c3ce

Browse files
authored
Merge pull request #18 from codebridger/CU-86exfn1e3_Implement-test-series-for-the-extension_Navid-Shad
implement test series for the extension navid shad #86exfn1e3
2 parents 534493c + ddeb5e4 commit 2f6c3ce

31 files changed

Lines changed: 2853 additions & 35 deletions

.github/workflows/release.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
branches:
66
- main
77
- dev
8+
pull_request:
9+
branches:
10+
- main
11+
- dev
812

913
permissions:
1014
contents: write
@@ -16,8 +20,96 @@ concurrency:
1620
cancel-in-progress: false
1721

1822
jobs:
23+
verify:
24+
name: Verify (lint, unit, build, e2e)
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v4
29+
with:
30+
persist-credentials: false
31+
32+
# Required because src/stores/profile.ts imports types from
33+
# ../../../dashboard-app/frontend/types/database.type — see CLAUDE.md.
34+
- name: Checkout sibling dashboard-app
35+
run: git clone --depth 1 https://github.com/codebridger/subturtle-dashboard-app.git ../dashboard-app
36+
37+
- name: Setup Node
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: 22
41+
cache: yarn
42+
43+
- name: Install dependencies
44+
run: yarn install --frozen-lockfile
45+
46+
- name: Cache Playwright browsers
47+
id: playwright-cache
48+
uses: actions/cache@v4
49+
with:
50+
path: ~/.cache/ms-playwright
51+
key: playwright-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
52+
restore-keys: |
53+
playwright-${{ runner.os }}-
54+
55+
- name: Install Playwright Chromium
56+
if: steps.playwright-cache.outputs.cache-hit != 'true'
57+
run: npx playwright install chromium --with-deps
58+
59+
- name: Install Playwright system deps (cache hit path)
60+
if: steps.playwright-cache.outputs.cache-hit == 'true'
61+
run: npx playwright install-deps chromium
62+
63+
- name: Type check
64+
run: yarn typecheck
65+
66+
- name: Unit tests (Vitest)
67+
run: yarn test
68+
69+
# dotenv-webpack is configured with `safe: true`, so the build needs
70+
# every key in .env.example to exist at build time. We write
71+
# non-empty placeholders rather than copying the empty .env.example
72+
# — `mixpanel.init("")` throws synchronously during the content-script
73+
# import chain, which silently halts every Vue mount and was the root
74+
# cause of e2e tests failing on `#subturtle-{nibble,console-crane}-root`
75+
# never appearing. SUBTURTLE_API_URL points at the local fixtures
76+
# server so any auth/translate calls 404 instead of escaping to the
77+
# real backend.
78+
- name: Stub .env.production for verify build
79+
run: |
80+
cat > .env.production <<'EOF'
81+
MIXPANEL_PROJECT_TOKEN=ci_e2e_stub_token
82+
MIXPANEL_API_HOST=http://localhost:4173/_mixpanel_stub
83+
GOOGLE_TRANSLATE_KEY=ci_e2e_stub_key
84+
GOOGLE_TRANSLATE_PROXY_URL=http://localhost:4173/_translate_proxy_stub
85+
UNINSTALL_FORM_URL=http://localhost:4173/_uninstall_stub
86+
SUBTURTLE_API_URL=http://localhost:4173
87+
SUBTURTLE_DASHBOARD_URL=http://localhost:4173/_dashboard_stub
88+
GOOGLE_OAUTH_CLIENT_ID=ci_e2e_stub_oauth_client
89+
EOF
90+
91+
- name: Build extension
92+
run: yarn build
93+
94+
- name: E2E tests (Playwright)
95+
run: yarn test:e2e
96+
97+
- name: Upload Playwright report
98+
# Run on both success and failure (anything except job cancel) so
99+
# the HTML report is downloadable for green runs too. The
100+
# hashFiles guard skips silently when typecheck or unit tests
101+
# failed before Playwright produced any output.
102+
if: ${{ !cancelled() && hashFiles('playwright-report/**') != '' }}
103+
uses: actions/upload-artifact@v4
104+
with:
105+
name: playwright-report
106+
path: playwright-report/
107+
retention-days: 7
108+
19109
release:
20110
name: Release
111+
needs: verify
112+
if: github.event_name == 'push'
21113
runs-on: ubuntu-latest
22114
environment: ${{ github.ref_name == 'main' && 'prod' || 'dev' }}
23115
steps:

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ dist
66
static/key-file.json
77
*.zip
88
.npmrc
9-
/.claude
9+
/.claude
10+
/playwright-report
11+
/test-results

CLAUDE.md

Lines changed: 127 additions & 13 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,29 @@
66
"dev": "webpack --watch",
77
"build": "NODE_ENV=production webpack --mode=production",
88
"zip": "cd dist && zip -r subturtle.zip . && mv subturtle.zip ../subturtle.zip",
9+
"test": "vitest run",
10+
"test:watch": "vitest",
11+
"test:e2e": "playwright test",
12+
"typecheck": "node scripts/typecheck.mjs",
913
"release": "semantic-release",
1014
"release:dry": "semantic-release --dry-run --no-ci"
1115
},
1216
"devDependencies": {
1317
"@egoist/tailwindcss-icons": "1.7.1",
1418
"@iconify/json": "2.2.165",
19+
"@pinia/testing": "^1",
20+
"@playwright/test": "^1.49",
1521
"@semantic-release/changelog": "^6.0.3",
1622
"@semantic-release/exec": "^7.1.0",
1723
"@semantic-release/git": "^10.0.1",
1824
"@types/chrome": "0.0.193",
1925
"@types/mixpanel-browser": "2.38.0",
26+
"@vitejs/plugin-vue": "^5",
27+
"@vue/test-utils": "^2",
2028
"copy-webpack-plugin": "11.0.0",
2129
"css-loader": "6.7.1",
2230
"dotenv-webpack": "8.0.1",
31+
"happy-dom": "^15",
2332
"json-loader": "0.5.7",
2433
"postcss": "8.4.16",
2534
"postcss-loader": "7.0.1",
@@ -32,6 +41,7 @@
3241
"tailwindcss": "3",
3342
"ts-loader": "9.5.1",
3443
"typescript": "5.4.5",
44+
"vitest": "^2",
3545
"vue-loader": "17.4.2",
3646
"vue-style-loader": "4.1.3",
3747
"vue-template-compiler": "2.7.16",

playwright.config.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineConfig } from "@playwright/test";
2+
3+
// E2E config — runs only against the Playwright specs in tests/e2e/.
4+
// Vitest is configured to exclude this directory so the two suites don't
5+
// fight over file ownership.
6+
//
7+
// `dist/` must be present before tests run; the e2e suite asserts artifacts
8+
// and loads the extension into Chromium. CI runs `yarn build` before
9+
// `yarn test:e2e` (see CLAUDE.md release pipeline notes).
10+
export default defineConfig({
11+
testDir: "./tests/e2e",
12+
testMatch: ["**/*.spec.ts"],
13+
fullyParallel: false,
14+
workers: 1,
15+
// Always emit the HTML report so CI failures have an artifact we can
16+
// upload and inspect (the verify workflow uploads playwright-report/
17+
// when a step fails). The list reporter stays so terminal output
18+
// remains readable.
19+
reporter: [["list"], ["html", { open: "never" }]],
20+
21+
use: {
22+
baseURL: "http://localhost:4173",
23+
trace: "retain-on-failure",
24+
},
25+
26+
webServer: {
27+
command: "node tests/e2e/server.mjs",
28+
url: "http://localhost:4173/",
29+
reuseExistingServer: !process.env.CI,
30+
stdout: "ignore",
31+
stderr: "pipe",
32+
timeout: 30_000,
33+
},
34+
});

scripts/typecheck.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Wrapper around `tsc --noEmit` that filters out two classes of upstream
2+
// errors we can't fix from this repo:
3+
//
4+
// 1. node_modules/pilotui/* — pilotui's package.json points `exports.types`
5+
// at raw TS source, so tsc follows into pilotui/src/vue.ts which has a
6+
// mismatched plugin signature against vue3-perfect-scrollbar.
7+
//
8+
// 2. ../dashboard-app/* — src/stores/profile.ts imports types from the
9+
// sibling dashboard-app repo (see CLAUDE.md). dashboard-app's frontend
10+
// types re-export from server-side TS that depends on mongoose / stripe
11+
// / @modular-rest/server — installed in dashboard-app's own
12+
// node_modules but not in ours. CI clones dashboard-app without
13+
// installing its deps; locally maintainers usually have them. Either
14+
// way, those errors aren't actionable from here.
15+
//
16+
// On clean runs we print only a short summary so GitHub's log parser
17+
// doesn't surface the suppressed errors as red `Error:` annotations.
18+
// Real errors in our own code still print the full tsc output and fail.
19+
import { spawnSync } from "node:child_process";
20+
21+
const SUPPRESSED_PATH_FRAGMENTS = [
22+
"node_modules/pilotui/",
23+
"dashboard-app/",
24+
];
25+
26+
const FILE_AT_ERROR = /([^\s:]+\.(?:ts|tsx|d\.ts|vue))\(\d+,\d+\):\s*error\s+TS\d+/;
27+
28+
function isSuppressed(line) {
29+
const m = line.match(FILE_AT_ERROR);
30+
if (!m) return false;
31+
return SUPPRESSED_PATH_FRAGMENTS.some((frag) => m[1].includes(frag));
32+
}
33+
34+
const r = spawnSync("npx", ["tsc", "--noEmit"], { encoding: "utf8" });
35+
const output = (r.stdout || "") + (r.stderr || "");
36+
37+
const allErrorLines = output.split("\n").filter((l) => /error TS\d+/.test(l));
38+
const realErrorLines = allErrorLines.filter((l) => !isSuppressed(l));
39+
const suppressedCount = allErrorLines.length - realErrorLines.length;
40+
41+
if (realErrorLines.length > 0) {
42+
// Print full tsc output so the user sees error context, then a summary.
43+
console.error(output);
44+
console.error(
45+
`\n${realErrorLines.length} type error(s) in our code. Fix above.`
46+
);
47+
if (suppressedCount > 0) {
48+
console.error(
49+
`(${suppressedCount} additional error(s) suppressed from pilotui / dashboard-app — see scripts/typecheck.mjs.)`
50+
);
51+
}
52+
process.exit(1);
53+
}
54+
55+
const suffix =
56+
suppressedCount > 0
57+
? ` (${suppressedCount} upstream error(s) from pilotui / dashboard-app suppressed — see scripts/typecheck.mjs)`
58+
: "";
59+
console.log(`typecheck clean.${suffix}`);
60+
process.exit(0);

src/background.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ function broadcastSettings(settings: SettingsObject) {
4242
// Tabs without our content script (chrome://, web store, freshly
4343
// installed pre-extension tabs, etc.) reject with "Receiving end does
4444
// not exist". That's expected for a fire-and-forget broadcast.
45-
chrome.tabs
46-
.sendMessage(tab.id, {
45+
// Cast: @types/chrome@0.0.193 still types tabs.sendMessage as void;
46+
// in MV3 the no-callback overload returns a Promise.
47+
(
48+
chrome.tabs.sendMessage(tab.id, {
4749
type: MESSAGE_TYPE.SYNC_SETTINGS,
4850
settings,
49-
})
50-
.catch(() => {});
51+
}) as unknown as Promise<unknown>
52+
).catch(() => {});
5153
}
5254
});
5355
}

src/console-crane/route-params.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
// `btoa` only accepts Latin1 — any non-Latin1 character (e.g. accented Latin,
77
// Persian, Chinese, emoji) throws InvalidCharacterError. We encode via
88
// TextEncoder so route params can carry any text.
9+
//
10+
// Undefined is a legitimate input — `toggleConsoleCrane(page)` calls this
11+
// without explicit params for routes like "empty" / "settings". We round-trip
12+
// it as an empty string so JSON.parse never sees an empty payload.
913
export function encodeRouteParams(params: any): string {
10-
const bytes = new TextEncoder().encode(JSON.stringify(params));
14+
const json = JSON.stringify(params);
15+
if (json === undefined) return "";
16+
const bytes = new TextEncoder().encode(json);
1117
let binary = "";
1218
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
1319
return btoa(binary);
1420
}
1521

16-
export function decodeRouteParams<T = any>(data: string): T {
22+
export function decodeRouteParams<T = any>(data: string): T | undefined {
23+
if (data === "") return undefined;
1724
const binary = atob(data);
1825
const bytes = new Uint8Array(binary.length);
1926
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

src/vue-shim.d.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
// Vue 3 SFC ambient declaration. The previous shim used Vue 2's default
2+
// export shape, which made TS infer the bare `vue` module namespace at every
3+
// .vue import site — see the 7 errors in src/console-crane/router.ts and
4+
// src/popup/router.ts.
15
declare module "*.vue" {
2-
import Vue from "vue";
3-
export default Vue;
4-
}
6+
import type { DefineComponent } from "vue";
7+
const component: DefineComponent<{}, {}, any>;
8+
export default component;
9+
}

0 commit comments

Comments
 (0)