Skip to content

Commit d3adc76

Browse files
authored
Consolidate extension storage under amg-state (#50)
## Summary - move extension state into a single `amg-state` storage key and migrate shared legacy state (`uiStore`, `platformInfo`, analytics fields, votes) - update background/popup/stores to read and write via centralized amg-state helpers - update e2e storage seeding and popup assertions to validate before/after domain state under `amg-state.domains` - increase local Playwright timeout from 5s to 10s to reduce transient local flakes Closes #36 ## Test plan - [x] `pnpm check` - [x] `pnpm lint` - [x] `pnpm exec playwright test -g "Per-domain activation|Toolbar settings menu"` - [x] `pnpm exec playwright test e2e/tests/popup.spec.ts` - [x] `pnpm test:e2e`
1 parent 117aa80 commit d3adc76

12 files changed

Lines changed: 706 additions & 69 deletions

File tree

.changeset/public-humans-smash.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"amgiflol": patch
3+
---
4+
5+
# Storage migration to amg-state
6+
7+
Consolidate extension storage under `amg-state` and migrate shared legacy state.
8+
9+
This updates popup/background/store bindings and E2E coverage for the new storage shape.
10+
Per-domain activation keys are not auto-migrated, so domains may need to be re-enabled once.

.cursor/skills/releases-changesets/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ All user-facing or notable changes get a changeset in [.changeset/](.changeset/)
1313

1414
- 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.
1515

16+
### Non-interactive automation
17+
18+
When automation must avoid interactive prompts:
19+
20+
1. Run `pnpm changeset add --empty --message "<summary>"`.
21+
2. Update the generated `.changeset/*.md` frontmatter to include the release bump, for example:
22+
- `"amgiflol": patch`
23+
3. Validate before finalizing:
24+
- `pnpm changeset status`
25+
- relevant project checks used by the current task (for example `pnpm check`, `pnpm lint`).
26+
27+
Notes:
28+
29+
- `--empty` creates an empty frontmatter block by default (`--- ---`) and does not select `patch/minor/major`.
30+
- `--message` only sets the summary text and does not set bump type.
31+
1632
## Config
1733

1834
[.changeset/config.json](.changeset/config.json): `changelog: "@changesets/cli/changelog"`, `commit: false`, `baseBranch: "main"`.

e2e/pages/web.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import path from "path";
44
declare const chrome: {
55
storage: {
66
local: {
7-
set: (items: Record<string, boolean>) => void | Promise<void>;
7+
get: (keys: string[]) => Promise<Record<string, unknown>>;
8+
set: (items: Record<string, unknown>) => void | Promise<void>;
89
};
910
};
1011
};
@@ -27,7 +28,42 @@ export async function enableDomainInStorage(context: BrowserContext, domain: str
2728
worker = await context.waitForEvent("serviceworker");
2829
}
2930
const setStorage = (d: string) => {
30-
return chrome.storage.local.set({ [d]: true });
31+
const toBooleanRecord = (value: unknown): Record<string, boolean> => {
32+
if (typeof value !== "object" || value === null) return {};
33+
const result: Record<string, boolean> = {};
34+
for (const [key, fieldValue] of Object.entries(value)) {
35+
if (typeof fieldValue === "boolean") {
36+
result[key] = fieldValue;
37+
}
38+
}
39+
return result;
40+
};
41+
const toUnknownRecord = (value: unknown): Record<string, unknown> => {
42+
if (typeof value !== "object" || value === null) return {};
43+
const result: Record<string, unknown> = {};
44+
for (const [key, fieldValue] of Object.entries(value)) {
45+
result[key] = fieldValue;
46+
}
47+
return result;
48+
};
49+
return chrome.storage.local.get(["amg-state"]).then((result) => {
50+
const currentState = result["amg-state"];
51+
const stateRecord = toUnknownRecord(currentState);
52+
const baseState = {
53+
analytics: toUnknownRecord(stateRecord.analytics),
54+
domains: toBooleanRecord(stateRecord.domains),
55+
votes: toBooleanRecord(stateRecord.votes),
56+
};
57+
return chrome.storage.local.set({
58+
"amg-state": {
59+
...baseState,
60+
domains: {
61+
...baseState.domains,
62+
[d]: true,
63+
},
64+
},
65+
});
66+
});
3167
};
3268
await worker.evaluate(setStorage, domain);
3369
}
@@ -50,5 +86,24 @@ export function getInspectorActiveMain(page: Page) {
5086
}
5187

5288
export async function expectSvelteAppLoaded(page: Page) {
53-
await page.locator("[data-amgiflol-root] >> main.active").waitFor({ state: "attached" });
89+
const timeoutMs = process.env.CI ? 24_000 : 12_000;
90+
const pollMs = 250;
91+
const startTime = Date.now();
92+
const root = getExtensionRoot(page).first();
93+
await root.waitFor({ state: "attached", timeout: timeoutMs });
94+
95+
while (Date.now() - startTime < timeoutMs) {
96+
if (page.isClosed()) {
97+
throw new Error("Page closed while waiting for extension app to activate.");
98+
}
99+
const activeMainCount = await getInspectorActiveMain(page).count();
100+
if (activeMainCount > 0) return;
101+
await page.waitForTimeout(pollMs);
102+
}
103+
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+
);
54109
}

e2e/tests/popup.spec.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
import pkg from "../../package.json" assert { type: "json" };
22
import { expect, test } from "../fixtures";
33

4+
function toUnknownRecord(value: unknown): Record<string, unknown> {
5+
if (typeof value !== "object" || value === null) return {};
6+
const result: Record<string, unknown> = {};
7+
for (const [key, fieldValue] of Object.entries(value)) {
8+
result[key] = fieldValue;
9+
}
10+
return result;
11+
}
12+
13+
function toBooleanRecord(value: unknown): Record<string, boolean> {
14+
const unknownRecord = toUnknownRecord(value);
15+
const result: Record<string, boolean> = {};
16+
for (const [key, fieldValue] of Object.entries(unknownRecord)) {
17+
if (typeof fieldValue === "boolean") {
18+
result[key] = fieldValue;
19+
}
20+
}
21+
return result;
22+
}
23+
424
test.describe("Popup", () => {
525
test("popup shows correct UI and updates storage when toggled", async ({
626
page,
@@ -31,11 +51,26 @@ test.describe("Popup", () => {
3151
const storageAfter = await page.evaluate(async () => {
3252
return browser.storage.local.get(null);
3353
});
34-
const changedKeys = Object.keys(storageAfter).filter(
35-
(key) => storageAfter[key] !== storageBefore[key],
36-
);
54+
expect(Object.hasOwn(storageBefore, "amg-state")).toBeTruthy();
55+
expect(Object.hasOwn(storageAfter, "amg-state")).toBeTruthy();
56+
57+
const domain = await page.evaluate(async () => {
58+
const [tab] = await browser.tabs.query({
59+
active: true,
60+
currentWindow: true,
61+
});
62+
if (!tab?.url) return "";
63+
return new URL(tab.url).host;
64+
});
65+
66+
const beforeAmgState = toUnknownRecord(storageBefore["amg-state"]);
67+
const afterAmgState = toUnknownRecord(storageAfter["amg-state"]);
68+
const beforeDomains = toBooleanRecord(beforeAmgState.domains);
69+
const afterDomains = toBooleanRecord(afterAmgState.domains);
70+
const beforeDomainValue = beforeDomains[domain];
71+
const afterDomainValue = afterDomains[domain];
3772

38-
expect(changedKeys.length).toBeGreaterThan(0);
39-
expect(changedKeys.some((key) => storageAfter[key] === true)).toBeTruthy();
73+
expect(beforeDomainValue === undefined || beforeDomainValue === false).toBeTruthy();
74+
expect(afterDomainValue).toBeTruthy();
4075
});
4176
});

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default defineConfig({
6161
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
6262
// },
6363
],
64-
timeout: process.env.CI ? 30_000 : 5_000,
64+
timeout: process.env.CI ? 30_000 : 10_000,
6565
webServer: {
6666
command: "node e2e/serve-fixtures.mjs 51234",
6767
url: "http://localhost:51234/inspector-playground.html",

src/app.config.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
// <srcDir>/app.config.ts
22
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
3+
import {
4+
createAnalyticsEnabledStorageItem,
5+
createAnalyticsUserIdStorageItem,
6+
createAnalyticsUserPropertiesStorageItem,
7+
} from "@/lib/storage/amgState";
38

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

@@ -12,15 +17,9 @@ export default defineAppConfig({
1217
measurementId: "G-XWT0HYT2KT",
1318
}),
1419
],
15-
enabled: storage.defineItem("local:analytics-enabled", {
16-
fallback: true,
17-
}),
18-
userId: storage.defineItem<string | undefined>("local:amg-user-id-key", {
19-
fallback: crypto.randomUUID().toString(),
20-
}),
21-
userProperties: storage.defineItem("local:amg-user-properties-key", {
22-
fallback: {},
23-
}),
20+
enabled: createAnalyticsEnabledStorageItem(),
21+
userId: createAnalyticsUserIdStorageItem(),
22+
userProperties: createAnalyticsUserPropertiesStorageItem(),
2423
},
2524
version: pkg.version.toString(),
2625
});

src/entrypoints/background.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import {
2+
getDomainActive,
3+
migrateLegacyState,
4+
setDomainActive,
5+
setPlatformInfoState,
6+
} from "@/lib/storage/amgState";
7+
18
const IconOff = {
29
16: "icons/icon-off-16.png",
310
32: "icons/icon-off-32.png",
@@ -19,23 +26,23 @@ export default defineBackground(() => {
1926
captureHandler(sender.tab);
2027
} else if (event.type === "EXTENSION_TOGGLE") {
2128
const { domain, isActive } = event.payload;
22-
browser.storage?.local.set({
23-
[domain]: isActive,
24-
});
29+
await setDomainActive(domain, isActive);
2530
setIcon(isActive);
2631
}
2732
});
2833

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

32-
savePlatformInfo();
37+
await migrateLegacyState();
38+
await savePlatformInfo();
3339
analytics.setEnabled(true);
3440
void updateIconForActiveTab();
3541
});
3642

3743
browser.runtime.onStartup.addListener(async () => {
3844
console.log("Extension started");
45+
await migrateLegacyState();
3946
void updateIconForActiveTab();
4047
});
4148

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

6370
async function savePlatformInfo() {
6471
const platformInfo = await browser.runtime.getPlatformInfo();
65-
void browser.storage.local.set({ platformInfo: platformInfo });
72+
await setPlatformInfoState(platformInfo);
6673
}
6774

6875
/**
@@ -95,8 +102,7 @@ async function getCurrentDomain(tabId?: number): Promise<string | null> {
95102
*/
96103
async function isDomainActive(domain: string): Promise<boolean> {
97104
try {
98-
const result = await browser.storage.local.get([domain]);
99-
return Boolean(result[domain]);
105+
return await getDomainActive(domain);
100106
} catch (error) {
101107
console.error("Failed to check domain state:", error);
102108
return true;

src/lib/modules/ExtenstionSettings/App.svelte

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
createMessageHandler,
77
sendMessage,
88
} from "@/lib/core/MessageBus";
9+
import { getDomainActive } from "@/lib/storage/amgState";
910
import { Switch } from "@ark-ui/svelte/switch";
1011
import { browser } from "wxt/browser";
1112
import "./app.css";
@@ -42,11 +43,7 @@
4243
const url = new URL(tab.url);
4344
domain = url.host;
4445
45-
const result = await browser.storage?.local.get([domain]);
46-
const value = result?.[domain];
47-
if (result !== undefined) {
48-
isActive = typeof value === "boolean" ? value : false;
49-
}
46+
isActive = await getDomainActive(domain);
5047
}
5148
5249
if (isActive === undefined) {

0 commit comments

Comments
 (0)