Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { BrowserAutomationBrowserIdSchema } from "@getpaseo/protocol/browser-automation/rpc-schemas";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import {
|
||||
applyBrowserPatch,
|
||||
type BrowserIndexState,
|
||||
type BrowserRecord,
|
||||
type BrowserRecordPatch,
|
||||
createBrowserRecord,
|
||||
normalizeBrowserUrl,
|
||||
removeBrowserFromIndex,
|
||||
sanitizeBrowsersForPersist,
|
||||
trimNonEmpty,
|
||||
} from "./state";
|
||||
|
||||
export type { BrowserRecord } from "./state";
|
||||
|
||||
interface BrowserStoreState extends BrowserIndexState {
|
||||
createBrowser: (input?: { initialUrl?: string }) => string;
|
||||
updateBrowser: (browserId: string, patch: BrowserRecordPatch) => void;
|
||||
removeBrowser: (browserId: string) => void;
|
||||
}
|
||||
|
||||
function createBrowserId(): string {
|
||||
let browserId: string;
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
browserId = globalThis.crypto.randomUUID();
|
||||
} else {
|
||||
const randomSuffix = Math.random().toString(16).slice(2) || "0";
|
||||
browserId = `${Date.now()}-${randomSuffix}`;
|
||||
}
|
||||
return BrowserAutomationBrowserIdSchema.parse(browserId);
|
||||
}
|
||||
|
||||
export const useBrowserStore = create<BrowserStoreState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
browsersById: {},
|
||||
createBrowser: (input) => {
|
||||
const browserId = createBrowserId();
|
||||
const record = createBrowserRecord({
|
||||
browserId,
|
||||
initialUrl: input?.initialUrl,
|
||||
now: Date.now(),
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
browsersById: {
|
||||
...state.browsersById,
|
||||
[browserId]: record,
|
||||
},
|
||||
}));
|
||||
|
||||
return browserId;
|
||||
},
|
||||
updateBrowser: (browserId, patch) => {
|
||||
set((state) => applyBrowserPatch(state, browserId, patch));
|
||||
},
|
||||
removeBrowser: (browserId) => {
|
||||
set((state) => removeBrowserFromIndex(state, browserId));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "workspace-browser-store",
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
partialize: (state) => sanitizeBrowsersForPersist(state),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function getBrowserRecord(browserId: string): BrowserRecord | null {
|
||||
const normalizedBrowserId = trimNonEmpty(browserId);
|
||||
if (!normalizedBrowserId) {
|
||||
return null;
|
||||
}
|
||||
return useBrowserStore.getState().browsersById[normalizedBrowserId] ?? null;
|
||||
}
|
||||
|
||||
export function createWorkspaceBrowser(input?: { initialUrl?: string }): {
|
||||
browserId: string;
|
||||
url: string;
|
||||
} {
|
||||
const browserId = useBrowserStore.getState().createBrowser(input);
|
||||
const record = getBrowserRecord(browserId);
|
||||
return {
|
||||
browserId,
|
||||
url: record?.url ?? normalizeBrowserUrl(input?.initialUrl),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWorkspaceBrowserUrl(value: string | null | undefined): string {
|
||||
return normalizeBrowserUrl(value);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyBrowserPatch,
|
||||
type BrowserIndexState,
|
||||
createBrowserRecord,
|
||||
normalizeBrowserUrl,
|
||||
removeBrowserFromIndex,
|
||||
sanitizeBrowsersForPersist,
|
||||
} from "./state";
|
||||
|
||||
function withRecords(records: ReturnType<typeof createBrowserRecord>[]): BrowserIndexState {
|
||||
return {
|
||||
browsersById: Object.fromEntries(records.map((record) => [record.browserId, record])),
|
||||
};
|
||||
}
|
||||
|
||||
describe("normalizeBrowserUrl", () => {
|
||||
it("normalizes local development hosts to http by default", () => {
|
||||
expect(normalizeBrowserUrl("localhost:8081")).toBe("http://localhost:8081");
|
||||
expect(normalizeBrowserUrl("localhost/path")).toBe("http://localhost/path");
|
||||
expect(normalizeBrowserUrl("127.0.0.1:3000/path")).toBe("http://127.0.0.1:3000/path");
|
||||
expect(normalizeBrowserUrl("192.168.0.8")).toBe("http://192.168.0.8");
|
||||
expect(normalizeBrowserUrl("[::1]:5173")).toBe("http://[::1]:5173");
|
||||
});
|
||||
|
||||
it("normalizes public hosts to https by default", () => {
|
||||
expect(normalizeBrowserUrl("example.com")).toBe("https://example.com");
|
||||
expect(normalizeBrowserUrl("//example.com/path")).toBe("https://example.com/path");
|
||||
});
|
||||
|
||||
it("keeps explicit protocols unchanged", () => {
|
||||
expect(normalizeBrowserUrl("http://localhost:8081")).toBe("http://localhost:8081");
|
||||
expect(normalizeBrowserUrl("https://localhost:8081")).toBe("https://localhost:8081");
|
||||
expect(normalizeBrowserUrl("file:///tmp/example.html")).toBe("file:///tmp/example.html");
|
||||
});
|
||||
|
||||
it("falls back to a default URL when input is blank", () => {
|
||||
expect(normalizeBrowserUrl(null)).toBe("https://example.com");
|
||||
expect(normalizeBrowserUrl(" ")).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createBrowserRecord", () => {
|
||||
it("normalizes the initial URL and starts with idle state", () => {
|
||||
const record = createBrowserRecord({
|
||||
browserId: "b1",
|
||||
initialUrl: "localhost:8081",
|
||||
now: 1000,
|
||||
});
|
||||
|
||||
expect(record).toEqual({
|
||||
browserId: "b1",
|
||||
url: "http://localhost:8081",
|
||||
title: "",
|
||||
isLoading: false,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
faviconUrl: null,
|
||||
lastError: null,
|
||||
createdAt: 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyBrowserPatch", () => {
|
||||
it("normalizes URL updates", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
const next = applyBrowserPatch(initial, "b1", { url: "example.com/path" });
|
||||
|
||||
expect(next.browsersById.b1?.url).toBe("https://example.com/path");
|
||||
});
|
||||
|
||||
it("returns the same state reference when nothing changes", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
const next = applyBrowserPatch(initial, "b1", { url: "https://a.test", title: "" });
|
||||
|
||||
expect(next).toBe(initial);
|
||||
});
|
||||
|
||||
it("returns the same state when the browser id is unknown", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
const next = applyBrowserPatch(initial, "missing", { title: "x" });
|
||||
|
||||
expect(next).toBe(initial);
|
||||
});
|
||||
|
||||
it("returns the same state when the browser id is blank", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
expect(applyBrowserPatch(initial, " ", { title: "x" })).toBe(initial);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeBrowserFromIndex", () => {
|
||||
it("removes the named browser", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
createBrowserRecord({ browserId: "b2", initialUrl: "https://b.test", now: 0 }),
|
||||
]);
|
||||
|
||||
const next = removeBrowserFromIndex(initial, "b1");
|
||||
|
||||
expect(Object.keys(next.browsersById)).toEqual(["b2"]);
|
||||
});
|
||||
|
||||
it("returns the same state when the browser id is unknown", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
expect(removeBrowserFromIndex(initial, "missing")).toBe(initial);
|
||||
});
|
||||
|
||||
it("returns the same state when the browser id is blank", () => {
|
||||
const initial = withRecords([
|
||||
createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 }),
|
||||
]);
|
||||
|
||||
expect(removeBrowserFromIndex(initial, " ")).toBe(initial);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeBrowsersForPersist", () => {
|
||||
it("clears transient fields on every record", () => {
|
||||
const base = createBrowserRecord({ browserId: "b1", initialUrl: "https://a.test", now: 0 });
|
||||
const state: BrowserIndexState = {
|
||||
browsersById: {
|
||||
b1: { ...base, isLoading: true, lastError: "network down" },
|
||||
},
|
||||
};
|
||||
|
||||
const persisted = sanitizeBrowsersForPersist(state);
|
||||
|
||||
expect(persisted.browsersById.b1?.isLoading).toBe(false);
|
||||
expect(persisted.browsersById.b1?.lastError).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
export interface BrowserRecord {
|
||||
browserId: string;
|
||||
url: string;
|
||||
title: string;
|
||||
isLoading: boolean;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
faviconUrl: string | null;
|
||||
lastError: string | null;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export type BrowserRecordPatch = Partial<Omit<BrowserRecord, "browserId" | "createdAt">>;
|
||||
|
||||
export interface BrowserIndexState {
|
||||
browsersById: Record<string, BrowserRecord>;
|
||||
}
|
||||
|
||||
export function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export function normalizeBrowserUrl(value: string | null | undefined): string {
|
||||
const trimmed = trimNonEmpty(value);
|
||||
if (!trimmed) {
|
||||
return "https://example.com";
|
||||
}
|
||||
if (/^(localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[\da-fA-F:.]+])(?::\d+)?(?:[/?#]|$)/.test(trimmed)) {
|
||||
return `http://${trimmed}`;
|
||||
}
|
||||
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
if (trimmed.startsWith("//")) {
|
||||
return `https:${trimmed}`;
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
}
|
||||
|
||||
export function createBrowserRecord(input: {
|
||||
browserId: string;
|
||||
initialUrl: string | null | undefined;
|
||||
now: number;
|
||||
}): BrowserRecord {
|
||||
return {
|
||||
browserId: input.browserId,
|
||||
url: normalizeBrowserUrl(input.initialUrl),
|
||||
title: "",
|
||||
isLoading: false,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
faviconUrl: null,
|
||||
lastError: null,
|
||||
createdAt: input.now,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyBrowserPatch<S extends BrowserIndexState>(
|
||||
state: S,
|
||||
browserId: string,
|
||||
patch: BrowserRecordPatch,
|
||||
): S {
|
||||
const normalizedBrowserId = trimNonEmpty(browserId);
|
||||
if (!normalizedBrowserId) {
|
||||
return state;
|
||||
}
|
||||
const existing = state.browsersById[normalizedBrowserId];
|
||||
if (!existing) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const nextRecord: BrowserRecord = {
|
||||
...existing,
|
||||
...patch,
|
||||
url: normalizeBrowserUrl(patch.url ?? existing.url),
|
||||
};
|
||||
|
||||
if (
|
||||
nextRecord.url === existing.url &&
|
||||
nextRecord.title === existing.title &&
|
||||
nextRecord.isLoading === existing.isLoading &&
|
||||
nextRecord.canGoBack === existing.canGoBack &&
|
||||
nextRecord.canGoForward === existing.canGoForward &&
|
||||
nextRecord.faviconUrl === existing.faviconUrl &&
|
||||
nextRecord.lastError === existing.lastError
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
browsersById: {
|
||||
...state.browsersById,
|
||||
[normalizedBrowserId]: nextRecord,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function removeBrowserFromIndex<S extends BrowserIndexState>(
|
||||
state: S,
|
||||
browserId: string,
|
||||
): S {
|
||||
const normalizedBrowserId = trimNonEmpty(browserId);
|
||||
if (!normalizedBrowserId) {
|
||||
return state;
|
||||
}
|
||||
if (!state.browsersById[normalizedBrowserId]) {
|
||||
return state;
|
||||
}
|
||||
const next = { ...state.browsersById };
|
||||
delete next[normalizedBrowserId];
|
||||
return { ...state, browsersById: next };
|
||||
}
|
||||
|
||||
export function sanitizeBrowsersForPersist(state: BrowserIndexState): {
|
||||
browsersById: Record<string, BrowserRecord>;
|
||||
} {
|
||||
return {
|
||||
browsersById: Object.fromEntries(
|
||||
Object.entries(state.browsersById).map(([browserId, browser]) => [
|
||||
browserId,
|
||||
{ ...browser, isLoading: false, lastError: null },
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user