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
10 changes: 10 additions & 0 deletions .changeset/public-humans-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"amgiflol": patch
---

# Storage migration to amg-state

Consolidate extension storage under `amg-state` and migrate shared legacy state.

This updates popup/background/store bindings and E2E coverage for the new storage shape.
Per-domain activation keys are not auto-migrated, so domains may need to be re-enabled once.
16 changes: 16 additions & 0 deletions .cursor/skills/releases-changesets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ All user-facing or notable changes get a changeset in [.changeset/](.changeset/)

- Run `pnpm changeset`, or add a new `.md` file in `.changeset/` with the standard format: semver type (major/minor/patch) and a short summary for the changelog.

### Non-interactive automation

When automation must avoid interactive prompts:

1. Run `pnpm changeset add --empty --message "<summary>"`.
2. Update the generated `.changeset/*.md` frontmatter to include the release bump, for example:
- `"amgiflol": patch`
3. Validate before finalizing:
- `pnpm changeset status`
- relevant project checks used by the current task (for example `pnpm check`, `pnpm lint`).

Notes:

- `--empty` creates an empty frontmatter block by default (`--- ---`) and does not select `patch/minor/major`.
- `--message` only sets the summary text and does not set bump type.

## Config

[.changeset/config.json](.changeset/config.json): `changelog: "@changesets/cli/changelog"`, `commit: false`, `baseBranch: "main"`.
Expand Down
61 changes: 58 additions & 3 deletions e2e/pages/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
declare const chrome: {
storage: {
local: {
set: (items: Record<string, boolean>) => void | Promise<void>;
get: (keys: string[]) => Promise<Record<string, unknown>>;
set: (items: Record<string, unknown>) => void | Promise<void>;
};
};
};
Expand All @@ -27,7 +28,42 @@
worker = await context.waitForEvent("serviceworker");
}
const setStorage = (d: string) => {
return chrome.storage.local.set({ [d]: true });
const toBooleanRecord = (value: unknown): Record<string, boolean> => {
if (typeof value !== "object" || value === null) return {};
const result: Record<string, boolean> = {};
for (const [key, fieldValue] of Object.entries(value)) {
if (typeof fieldValue === "boolean") {
result[key] = fieldValue;
}
}
return result;
};
const toUnknownRecord = (value: unknown): Record<string, unknown> => {
if (typeof value !== "object" || value === null) return {};
const result: Record<string, unknown> = {};
for (const [key, fieldValue] of Object.entries(value)) {
result[key] = fieldValue;
}
return result;
};
return chrome.storage.local.get(["amg-state"]).then((result) => {
const currentState = result["amg-state"];
const stateRecord = toUnknownRecord(currentState);
const baseState = {
analytics: toUnknownRecord(stateRecord.analytics),
domains: toBooleanRecord(stateRecord.domains),
votes: toBooleanRecord(stateRecord.votes),
};
return chrome.storage.local.set({
"amg-state": {
...baseState,
domains: {
...baseState.domains,
[d]: true,
},
},
});
});
};
await worker.evaluate(setStorage, domain);
}
Expand All @@ -50,5 +86,24 @@
}

export async function expectSvelteAppLoaded(page: Page) {
await page.locator("[data-amgiflol-root] >> main.active").waitFor({ state: "attached" });
const timeoutMs = process.env.CI ? 24_000 : 12_000;
const pollMs = 250;
const startTime = Date.now();
const root = getExtensionRoot(page).first();
await root.waitFor({ state: "attached", timeout: timeoutMs });

while (Date.now() - startTime < timeoutMs) {
if (page.isClosed()) {
throw new Error("Page closed while waiting for extension app to activate.");
}
const activeMainCount = await getInspectorActiveMain(page).count();
if (activeMainCount > 0) return;
await page.waitForTimeout(pollMs);
}

const rootCount = await getExtensionRoot(page).count();
const mainCount = await getSvelteAppMain(page).count();
throw new Error(

Check failure on line 106 in e2e/pages/web.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › e2e/tests/toolbar-overlays.spec.ts:11:2 › Toolbar overlays › grid overlay can be toggled

3) [chromium] › e2e/tests/toolbar-overlays.spec.ts:11:2 › Toolbar overlays › grid overlay can be toggled Error: Extension app did not activate within 24000ms (rootCount=1, mainCount=1). at pages/web.ts:106 104 | const rootCount = await getExtensionRoot(page).count(); 105 | const mainCount = await getSvelteAppMain(page).count(); > 106 | throw new Error( | ^ 107 | `Extension app did not activate within ${timeoutMs}ms (rootCount=${rootCount}, mainCount=${mainCount}).`, 108 | ); 109 | } at expectSvelteAppLoaded (/home/runner/work/amgiflol/amgiflol/e2e/pages/web.ts:106:8) at /home/runner/work/amgiflol/amgiflol/e2e/tests/toolbar-overlays.spec.ts:15:3

Check failure on line 106 in e2e/pages/web.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › e2e/tests/toolbar-distances.spec.ts:11:2 › Toolbar distances › distance lines appear after hover when enabled

2) [chromium] › e2e/tests/toolbar-distances.spec.ts:11:2 › Toolbar distances › distance lines appear after hover when enabled Error: Extension app did not activate within 24000ms (rootCount=1, mainCount=1). at pages/web.ts:106 104 | const rootCount = await getExtensionRoot(page).count(); 105 | const mainCount = await getSvelteAppMain(page).count(); > 106 | throw new Error( | ^ 107 | `Extension app did not activate within ${timeoutMs}ms (rootCount=${rootCount}, mainCount=${mainCount}).`, 108 | ); 109 | } at expectSvelteAppLoaded (/home/runner/work/amgiflol/amgiflol/e2e/pages/web.ts:106:8) at /home/runner/work/amgiflol/amgiflol/e2e/tests/toolbar-distances.spec.ts:19:3

Check failure on line 106 in e2e/pages/web.ts

View workflow job for this annotation

GitHub Actions / test

[chromium] › e2e/tests/sidepanel.spec.ts:11:2 › Side panel › side panel can be shown from toolbar

1) [chromium] › e2e/tests/sidepanel.spec.ts:11:2 › Side panel › side panel can be shown from toolbar Error: Extension app did not activate within 24000ms (rootCount=1, mainCount=1). at pages/web.ts:106 104 | const rootCount = await getExtensionRoot(page).count(); 105 | const mainCount = await getSvelteAppMain(page).count(); > 106 | throw new Error( | ^ 107 | `Extension app did not activate within ${timeoutMs}ms (rootCount=${rootCount}, mainCount=${mainCount}).`, 108 | ); 109 | } at expectSvelteAppLoaded (/home/runner/work/amgiflol/amgiflol/e2e/pages/web.ts:106:8) at /home/runner/work/amgiflol/amgiflol/e2e/tests/sidepanel.spec.ts:19:3
`Extension app did not activate within ${timeoutMs}ms (rootCount=${rootCount}, mainCount=${mainCount}).`,
);
}
45 changes: 40 additions & 5 deletions e2e/tests/popup.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import pkg from "../../package.json" assert { type: "json" };
import { expect, test } from "../fixtures";

function toUnknownRecord(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null) return {};
const result: Record<string, unknown> = {};
for (const [key, fieldValue] of Object.entries(value)) {
result[key] = fieldValue;
}
return result;
}

function toBooleanRecord(value: unknown): Record<string, boolean> {
const unknownRecord = toUnknownRecord(value);
const result: Record<string, boolean> = {};
for (const [key, fieldValue] of Object.entries(unknownRecord)) {
if (typeof fieldValue === "boolean") {
result[key] = fieldValue;
}
}
return result;
}

test.describe("Popup", () => {
test("popup shows correct UI and updates storage when toggled", async ({
page,
Expand Down Expand Up @@ -31,11 +51,26 @@ test.describe("Popup", () => {
const storageAfter = await page.evaluate(async () => {
return browser.storage.local.get(null);
});
const changedKeys = Object.keys(storageAfter).filter(
(key) => storageAfter[key] !== storageBefore[key],
);
expect(Object.hasOwn(storageBefore, "amg-state")).toBeTruthy();
expect(Object.hasOwn(storageAfter, "amg-state")).toBeTruthy();

const domain = await page.evaluate(async () => {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!tab?.url) return "";
return new URL(tab.url).host;
});

const beforeAmgState = toUnknownRecord(storageBefore["amg-state"]);
const afterAmgState = toUnknownRecord(storageAfter["amg-state"]);
const beforeDomains = toBooleanRecord(beforeAmgState.domains);
const afterDomains = toBooleanRecord(afterAmgState.domains);
const beforeDomainValue = beforeDomains[domain];
const afterDomainValue = afterDomains[domain];

expect(changedKeys.length).toBeGreaterThan(0);
expect(changedKeys.some((key) => storageAfter[key] === true)).toBeTruthy();
expect(beforeDomainValue === undefined || beforeDomainValue === false).toBeTruthy();
expect(afterDomainValue).toBeTruthy();
});
});
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
timeout: process.env.CI ? 30_000 : 5_000,
timeout: process.env.CI ? 30_000 : 10_000,
webServer: {
command: "node e2e/serve-fixtures.mjs 51234",
url: "http://localhost:51234/inspector-playground.html",
Expand Down
17 changes: 8 additions & 9 deletions src/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// <srcDir>/app.config.ts
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
import {
createAnalyticsEnabledStorageItem,
createAnalyticsUserIdStorageItem,
createAnalyticsUserPropertiesStorageItem,
} from "@/lib/storage/amgState";

import pkg from "../package.json";

Expand All @@ -12,15 +17,9 @@ export default defineAppConfig({
measurementId: "G-XWT0HYT2KT",
}),
],
enabled: storage.defineItem("local:analytics-enabled", {
fallback: true,
}),
userId: storage.defineItem<string | undefined>("local:amg-user-id-key", {
fallback: crypto.randomUUID().toString(),
}),
userProperties: storage.defineItem("local:amg-user-properties-key", {
fallback: {},
}),
enabled: createAnalyticsEnabledStorageItem(),
userId: createAnalyticsUserIdStorageItem(),
userProperties: createAnalyticsUserPropertiesStorageItem(),
},
version: pkg.version.toString(),
});
20 changes: 13 additions & 7 deletions src/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
getDomainActive,
migrateLegacyState,
setDomainActive,
setPlatformInfoState,
} from "@/lib/storage/amgState";

const IconOff = {
16: "icons/icon-off-16.png",
32: "icons/icon-off-32.png",
Expand All @@ -19,23 +26,23 @@ export default defineBackground(() => {
captureHandler(sender.tab);
} else if (event.type === "EXTENSION_TOGGLE") {
const { domain, isActive } = event.payload;
browser.storage?.local.set({
[domain]: isActive,
});
await setDomainActive(domain, isActive);
setIcon(isActive);
}
});

browser.runtime.onInstalled.addListener(async ({ reason }) => {
console.log(`Extension installed, reason: ${reason}, browser: ${import.meta.env.BROWSER}`);

savePlatformInfo();
await migrateLegacyState();
await savePlatformInfo();
analytics.setEnabled(true);
void updateIconForActiveTab();
});

browser.runtime.onStartup.addListener(async () => {
console.log("Extension started");
await migrateLegacyState();
void updateIconForActiveTab();
});

Expand All @@ -62,7 +69,7 @@ export default defineBackground(() => {

async function savePlatformInfo() {
const platformInfo = await browser.runtime.getPlatformInfo();
void browser.storage.local.set({ platformInfo: platformInfo });
await setPlatformInfoState(platformInfo);
}

/**
Expand Down Expand Up @@ -95,8 +102,7 @@ async function getCurrentDomain(tabId?: number): Promise<string | null> {
*/
async function isDomainActive(domain: string): Promise<boolean> {
try {
const result = await browser.storage.local.get([domain]);
return Boolean(result[domain]);
return await getDomainActive(domain);
} catch (error) {
console.error("Failed to check domain state:", error);
return true;
Expand Down
7 changes: 2 additions & 5 deletions src/lib/modules/ExtenstionSettings/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
createMessageHandler,
sendMessage,
} from "@/lib/core/MessageBus";
import { getDomainActive } from "@/lib/storage/amgState";
import { Switch } from "@ark-ui/svelte/switch";
import { browser } from "wxt/browser";
import "./app.css";
Expand Down Expand Up @@ -42,11 +43,7 @@
const url = new URL(tab.url);
domain = url.host;

const result = await browser.storage?.local.get([domain]);
const value = result?.[domain];
if (result !== undefined) {
isActive = typeof value === "boolean" ? value : false;
}
isActive = await getDomainActive(domain);
}

if (isActive === undefined) {
Expand Down
Loading
Loading