Skip to content

Commit 46fa80d

Browse files
committed
🛠️ [fix] Improve macOS smoke test reliability
Improves the reliability of the macOS smoke test by enhancing output directory resolution and ensuring test API availability. - 🔍 Updates the smoke test to search for the application in both `release` and `dist` directories, and also allows specifying a custom directory via environment variable. - Previously, the test only checked the `release` directory, causing failures when the application was built to a different location. - 🧪 Adds checks to ensure that Vitest's test API (e.g., `expect`, `describe`, `it`) is available in the global scope during tests. - This mitigates issues where the test API might not be properly initialized, leading to test failures. - 💾 Mocks `localStorage` and `sessionStorage` to prevent errors during tests and restores them after each test. - ➕ Adds `.last-run.json` to `.gitignore` to prevent it from being committed. [skip-ci] [ci-skip] Signed-off-by: Nick2bad4u <[email protected]>
1 parent c3f13de commit 46fa80d

File tree

3 files changed

+189
-7
lines changed

3 files changed

+189
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,4 @@ electron-app/.stylelintignore
358358
electron-app/.env.development
359359
electron-app/.env.production
360360
html/
361+
electron-app/test-results/.last-run.json

electron-app/scripts/run-mac-smoke-test.cjs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ const path = require("node:path");
99

1010
const PROJECT_ROOT = path.resolve(__dirname, "..");
1111
const REPO_ROOT = path.resolve(PROJECT_ROOT, "..");
12-
const RELEASE_DIR = path.resolve(PROJECT_ROOT, "release");
12+
const CUSTOM_OUTPUT_DIR = process.env.FFV_SMOKE_BUILD_DIR?.trim();
13+
/** @type {string[]} */
14+
const OUTPUT_DIR_CANDIDATES = [];
15+
16+
if (CUSTOM_OUTPUT_DIR) {
17+
OUTPUT_DIR_CANDIDATES.push(path.resolve(PROJECT_ROOT, CUSTOM_OUTPUT_DIR));
18+
}
19+
20+
OUTPUT_DIR_CANDIDATES.push(
21+
path.resolve(PROJECT_ROOT, "release"),
22+
path.resolve(PROJECT_ROOT, "dist"),
23+
);
1324
const SAMPLE_FIT = process.env.FFV_SMOKE_SAMPLE ?? path.join(REPO_ROOT, "fit-test-files", "17326739450_ACTIVITY.fit");
1425
const SMOKE_TIMEOUT_MS = Number(process.env.FFV_SMOKE_TEST_TIMEOUT_MS || 120_000);
1526

@@ -47,13 +58,16 @@ async function main() {
4758
throw new Error(`[mac-smoke-test] Sample FIT file not found at ${SAMPLE_FIT}`);
4859
}
4960

50-
if (!fs.existsSync(RELEASE_DIR) || !fs.statSync(RELEASE_DIR).isDirectory()) {
51-
throw new Error(`[mac-smoke-test] Release directory missing at ${RELEASE_DIR}`);
61+
const outputDir = resolveOutputDirectory();
62+
if (!outputDir) {
63+
throw new Error(`[mac-smoke-test] Packaged output directory missing. Checked: ${OUTPUT_DIR_CANDIDATES.join(", ")}`);
5264
}
5365

54-
const executablePath = findExecutable(RELEASE_DIR);
66+
console.info(`[mac-smoke-test] Inspecting packaged artifacts in ${outputDir}`);
67+
68+
const executablePath = findExecutable(outputDir);
5569
if (!executablePath) {
56-
throw new Error(`[mac-smoke-test] Unable to locate packaged macOS binary under ${RELEASE_DIR}`);
70+
throw new Error(`[mac-smoke-test] Unable to locate packaged macOS binary under ${outputDir}`);
5771
}
5872

5973
console.info(`[mac-smoke-test] Launching ${executablePath}`);
@@ -108,6 +122,23 @@ async function main() {
108122
console.info("[mac-smoke-test] Smoke test completed successfully.");
109123
}
110124

125+
/**
126+
* Determine which packaged output directory is available for the smoke test.
127+
* @returns {string|null}
128+
*/
129+
function resolveOutputDirectory() {
130+
for (const candidate of OUTPUT_DIR_CANDIDATES) {
131+
try {
132+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
133+
return candidate;
134+
}
135+
} catch {
136+
// Ignore filesystem errors so we can continue scanning.
137+
}
138+
}
139+
return null;
140+
}
141+
111142
main().catch((error) => {
112143
console.error(error instanceof Error ? error.message : String(error));
113144
process.exitCode = 1;

electron-app/tests/setupVitest.js

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
// @ts-nocheck
22
// Mock Leaflet global L for all Vitest tests
33
import { JSDOM } from "jsdom";
4-
import { vi, afterEach as vitestAfterEach, beforeEach as vitestBeforeEach, afterAll as vitestAfterAll } from "vitest";
4+
import {
5+
vi,
6+
afterEach as vitestAfterEach,
7+
beforeEach as vitestBeforeEach,
8+
afterAll as vitestAfterAll,
9+
beforeAll as vitestBeforeAll,
10+
describe as vitestDescribe,
11+
it as vitestIt,
12+
test as vitestTest,
13+
expect as vitestExpect,
14+
} from "vitest";
515
// Soft import of state manager test-only resets; guarded to avoid module init cost when not present
616
/** @type {undefined | (() => void)} */
717
let __resetStateMgr;
@@ -144,13 +154,143 @@ function ensureConsoleAlive() {
144154
}
145155
}
146156

157+
function ensureGlobalTestApi() {
158+
try {
159+
if (typeof globalThis.expect !== "function") {
160+
globalThis.expect = vitestExpect;
161+
}
162+
if (typeof globalThis.describe !== "function") {
163+
globalThis.describe = vitestDescribe;
164+
}
165+
if (typeof globalThis.it !== "function") {
166+
globalThis.it = vitestIt;
167+
}
168+
if (typeof globalThis.test !== "function") {
169+
globalThis.test = vitestTest;
170+
}
171+
if (typeof globalThis.beforeEach !== "function") {
172+
globalThis.beforeEach = vitestBeforeEach;
173+
}
174+
if (typeof globalThis.afterEach !== "function") {
175+
globalThis.afterEach = vitestAfterEach;
176+
}
177+
if (typeof globalThis.beforeAll !== "function") {
178+
globalThis.beforeAll = vitestBeforeAll;
179+
}
180+
if (typeof globalThis.afterAll !== "function") {
181+
globalThis.afterAll = vitestAfterAll;
182+
}
183+
} catch {
184+
/* ignore */
185+
}
186+
}
187+
188+
ensureGlobalTestApi();
189+
190+
class MemoryStorage {
191+
constructor() {
192+
this._store = new Map();
193+
}
194+
195+
getItem(key) {
196+
const normalized = String(key);
197+
return this._store.has(normalized) ? this._store.get(normalized) : null;
198+
}
199+
200+
setItem(key, value) {
201+
this._store.set(String(key), String(value));
202+
}
203+
204+
removeItem(key) {
205+
this._store.delete(String(key));
206+
}
207+
208+
clear() {
209+
this._store.clear();
210+
}
211+
212+
key(index) {
213+
return Array.from(this._store.keys())[Number(index)] ?? null;
214+
}
215+
216+
get length() {
217+
return this._store.size;
218+
}
219+
}
220+
221+
function isStorageValid(candidate) {
222+
return (
223+
!!candidate &&
224+
typeof candidate.getItem === "function" &&
225+
typeof candidate.setItem === "function" &&
226+
typeof candidate.removeItem === "function" &&
227+
typeof candidate.clear === "function"
228+
);
229+
}
230+
231+
function captureStorage(key) {
232+
try {
233+
const fromGlobal = /** @type {any} */ (globalThis)[key];
234+
if (isStorageValid(fromGlobal)) {
235+
return fromGlobal;
236+
}
237+
} catch {
238+
/* ignore */
239+
}
240+
try {
241+
if (typeof window !== "undefined") {
242+
const fromWindow = /** @type {any} */ (window)[key];
243+
if (isStorageValid(fromWindow)) {
244+
return fromWindow;
245+
}
246+
}
247+
} catch {
248+
/* ignore */
249+
}
250+
return undefined;
251+
}
252+
253+
function assignStorage(target, key, storage) {
254+
if (!target) return;
255+
try {
256+
Object.defineProperty(target, key, {
257+
configurable: true,
258+
enumerable: true,
259+
writable: true,
260+
value: storage,
261+
});
262+
} catch {
263+
try {
264+
target[key] = storage;
265+
} catch {
266+
/* ignore */
267+
}
268+
}
269+
}
270+
271+
function restoreStorage(key, initial) {
272+
const replacement = isStorageValid(initial) ? initial : new MemoryStorage();
273+
assignStorage(globalThis, key, replacement);
274+
if (typeof window !== "undefined") {
275+
assignStorage(window, key, replacement);
276+
}
277+
return replacement;
278+
}
279+
280+
const __initialLocalStorage = captureStorage("localStorage");
281+
const __initialSessionStorage = captureStorage("sessionStorage");
282+
283+
restoreStorage("localStorage", __initialLocalStorage);
284+
restoreStorage("sessionStorage", __initialSessionStorage);
285+
147286
// Register hooks defensively: in some execution modes (e.g., forks pool or
148287
// early setup evaluation), the Vitest runner might not yet be ready, causing
149288
// "Vitest failed to find the runner". Swallow those cases and proceed; tests
150289
// will still run, and other suites can register hooks when the runner is ready.
151290
try {
152291
vitestBeforeEach(() => {
153292
ensureConsoleAlive();
293+
ensureGlobalTestApi();
154294
try {
155295
const g = /** @type {any} */ (globalThis);
156296
if (!g.process || typeof g.process !== "object") g.process = {};
@@ -167,13 +307,16 @@ try {
167307
} catch {
168308
/* ignore */
169309
}
310+
restoreStorage("localStorage", __initialLocalStorage);
311+
restoreStorage("sessionStorage", __initialSessionStorage);
170312
});
171313
} catch {
172314
/* ignore: runner not yet available */
173315
}
174316
try {
175317
vitestAfterEach(() => {
176318
ensureConsoleAlive();
319+
ensureGlobalTestApi();
177320
try {
178321
const g = /** @type {any} */ (globalThis);
179322
if (!g.process || typeof g.process !== "object") g.process = {};
@@ -190,12 +333,19 @@ try {
190333
} catch {
191334
/* ignore */
192335
}
336+
restoreStorage("localStorage", __initialLocalStorage);
337+
restoreStorage("sessionStorage", __initialSessionStorage);
193338
});
194339
} catch {
195340
/* ignore: runner not yet available */
196341
}
197342
try {
198-
vitestAfterAll(() => ensureConsoleAlive());
343+
vitestAfterAll(() => {
344+
ensureConsoleAlive();
345+
ensureGlobalTestApi();
346+
restoreStorage("localStorage", __initialLocalStorage);
347+
restoreStorage("sessionStorage", __initialSessionStorage);
348+
});
199349
} catch {
200350
/* ignore: runner not yet available */
201351
}

0 commit comments

Comments
 (0)