Skip to content

Commit 15d9454

Browse files
authored
Merge pull request #49 from ryanbas21/feat/light-mode
feat: light mode, Payload tab, and bug fixes
2 parents 7021ad6 + a5b2695 commit 15d9454

5 files changed

Lines changed: 258 additions & 1 deletion

File tree

e2e/tests/import-error.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { test, expect } from '../fixtures/extension.js';
2+
import { openPanelPage } from '../helpers/panel-page.js';
3+
4+
test.describe('import error handling', () => {
5+
test('submitting invalid JSON shows error banner', async ({ extensionContext, extensionId }) => {
6+
const panelPage = await extensionContext.newPage();
7+
await openPanelPage(panelPage, extensionId);
8+
9+
// Open import paste area
10+
const importBtn = panelPage.locator('.tb-btn', { hasText: 'Import' });
11+
await importBtn.click();
12+
13+
const pasteArea = panelPage.locator('.import-paste-textarea');
14+
await expect(pasteArea).toBeVisible();
15+
16+
// Paste invalid JSON
17+
await pasteArea.fill('not valid json {{{');
18+
const submitBtn = panelPage.locator('.import-paste .tb-btn', { hasText: 'Import' });
19+
await submitBtn.click();
20+
21+
// Error banner should appear
22+
const errBanner = panelPage.locator('.err-banner');
23+
await expect(errBanner).toBeVisible({ timeout: 3000 });
24+
25+
// Panel should remain functional
26+
await expect(panelPage.locator('.toolbar')).toBeVisible();
27+
28+
await panelPage.close();
29+
});
30+
31+
test('submitting valid JSON with wrong schema shows error banner', async ({
32+
extensionContext,
33+
extensionId,
34+
}) => {
35+
const panelPage = await extensionContext.newPage();
36+
await openPanelPage(panelPage, extensionId);
37+
38+
// Open import paste area
39+
const importBtn = panelPage.locator('.tb-btn', { hasText: 'Import' });
40+
await importBtn.click();
41+
42+
const pasteArea = panelPage.locator('.import-paste-textarea');
43+
await expect(pasteArea).toBeVisible();
44+
45+
// Paste valid JSON but wrong schema (missing required fields)
46+
await pasteArea.fill(JSON.stringify({ version: 999, flow: {} }));
47+
const submitBtn = panelPage.locator('.import-paste .tb-btn', { hasText: 'Import' });
48+
await submitBtn.click();
49+
50+
// Error banner should appear
51+
const errBanner = panelPage.locator('.err-banner');
52+
await expect(errBanner).toBeVisible({ timeout: 3000 });
53+
54+
// Panel should remain functional
55+
await expect(panelPage.locator('.toolbar')).toBeVisible();
56+
57+
await panelPage.close();
58+
});
59+
});

e2e/tests/payload-tab.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { test, expect } from '../fixtures/extension.js';
2+
import { openPanelPage } from '../helpers/panel-page.js';
3+
import {
4+
injectDiscovery,
5+
injectTokenRequest,
6+
reloadAndWaitForEvents,
7+
} from '../helpers/inject-events.js';
8+
9+
test.describe('payload tab', () => {
10+
test('Payload tab appears for network events with request/response body', async ({
11+
extensionContext,
12+
extensionId,
13+
mockServer,
14+
}) => {
15+
const panelPage = await extensionContext.newPage();
16+
await openPanelPage(panelPage, extensionId);
17+
18+
// Inject discovery + token request (token has postData and JSON response body)
19+
await injectDiscovery(panelPage, mockServer.baseUrl);
20+
await injectTokenRequest(panelPage, mockServer.baseUrl);
21+
await reloadAndWaitForEvents(panelPage, 2);
22+
23+
// Select the token event (second row, after discovery)
24+
await panelPage.locator('.tl-row').nth(1).click();
25+
26+
// Payload tab should be visible (event has requestBody + responseBody)
27+
const payloadTab = panelPage.locator('.tab-btn', { hasText: 'Payload' });
28+
await expect(payloadTab).toBeVisible({ timeout: 3000 });
29+
30+
// Click the Payload tab
31+
await payloadTab.click();
32+
await expect(payloadTab).toHaveClass(/active/);
33+
34+
// Should show payload sections
35+
await expect(panelPage.locator('.payload-section').first()).toBeVisible();
36+
await expect(panelPage.locator('.sect-hdr', { hasText: 'Request Body' })).toBeVisible();
37+
await expect(panelPage.locator('.sect-hdr', { hasText: 'Response Body' })).toBeVisible();
38+
39+
await panelPage.close();
40+
});
41+
42+
test('Payload tab for discovery events shows only Response Body section', async ({
43+
extensionContext,
44+
extensionId,
45+
mockServer,
46+
}) => {
47+
const panelPage = await extensionContext.newPage();
48+
await openPanelPage(panelPage, extensionId);
49+
50+
// Discovery is a GET request with no postData — only has responseBody
51+
await injectDiscovery(panelPage, mockServer.baseUrl);
52+
await injectTokenRequest(panelPage, mockServer.baseUrl);
53+
await reloadAndWaitForEvents(panelPage, 2);
54+
55+
// Select the discovery event (first row) — it's a GET with no postData
56+
await panelPage.locator('.tl-row').first().click();
57+
58+
// Discovery has responseBody (the OIDC config JSON), so Payload tab
59+
// should appear. But it has no requestBody, so only Response Body shows.
60+
const payloadTab = panelPage.locator('.tab-btn', { hasText: 'Payload' });
61+
await expect(payloadTab).toBeVisible({ timeout: 3000 });
62+
await payloadTab.click();
63+
await expect(payloadTab).toHaveClass(/active/);
64+
await expect(panelPage.locator('.sect-hdr', { hasText: 'Response Body' })).toBeVisible();
65+
await expect(panelPage.locator('.sect-hdr', { hasText: 'Request Body' })).not.toBeVisible();
66+
67+
await panelPage.close();
68+
});
69+
});

e2e/tests/theme-toggle.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { test, expect } from '../fixtures/extension.js';
2+
import { openPanelPage } from '../helpers/panel-page.js';
3+
4+
test.describe('theme toggle', () => {
5+
test('theme toggle button is visible in toolbar', async ({ extensionContext, extensionId }) => {
6+
const panelPage = await extensionContext.newPage();
7+
await openPanelPage(panelPage, extensionId);
8+
9+
// The theme toggle is appended by JS after Elm renders the toolbar
10+
const toggleBtn = panelPage.locator('.theme-toggle');
11+
await expect(toggleBtn).toBeVisible({ timeout: 3000 });
12+
13+
await panelPage.close();
14+
});
15+
16+
test('clicking toggle switches to light mode and back', async ({
17+
extensionContext,
18+
extensionId,
19+
}) => {
20+
const panelPage = await extensionContext.newPage();
21+
await openPanelPage(panelPage, extensionId);
22+
23+
const toggleBtn = panelPage.locator('.theme-toggle');
24+
await expect(toggleBtn).toBeVisible({ timeout: 3000 });
25+
26+
// Get initial theme state
27+
const initialTheme = await panelPage.evaluate(() =>
28+
document.documentElement.getAttribute('data-theme'),
29+
);
30+
31+
// Click to switch theme
32+
await toggleBtn.click();
33+
const afterFirstClick = await panelPage.evaluate(() =>
34+
document.documentElement.getAttribute('data-theme'),
35+
);
36+
37+
if (initialTheme === null) {
38+
expect(afterFirstClick).toBe('light');
39+
} else {
40+
expect(afterFirstClick).toBeNull();
41+
}
42+
43+
// Click again to toggle back
44+
await toggleBtn.click();
45+
const afterSecondClick = await panelPage.evaluate(() =>
46+
document.documentElement.getAttribute('data-theme'),
47+
);
48+
expect(afterSecondClick).toBe(initialTheme);
49+
50+
await panelPage.close();
51+
});
52+
53+
test('theme preference persists across page reloads', async ({
54+
extensionContext,
55+
extensionId,
56+
}) => {
57+
const panelPage = await extensionContext.newPage();
58+
await openPanelPage(panelPage, extensionId);
59+
60+
const toggleBtn = panelPage.locator('.theme-toggle');
61+
await expect(toggleBtn).toBeVisible({ timeout: 3000 });
62+
63+
// Get initial state
64+
const initialTheme = await panelPage.evaluate(() =>
65+
document.documentElement.getAttribute('data-theme'),
66+
);
67+
68+
// Toggle the theme
69+
await toggleBtn.click();
70+
const newTheme = await panelPage.evaluate(() =>
71+
document.documentElement.getAttribute('data-theme'),
72+
);
73+
expect(newTheme).not.toBe(initialTheme);
74+
75+
// Verify localStorage was updated
76+
const storedTheme = await panelPage.evaluate(() => localStorage.getItem('wolfcola:theme'));
77+
expect(storedTheme).toBe(newTheme === 'light' ? 'light' : 'dark');
78+
79+
// Reload and verify theme persists
80+
await panelPage.reload();
81+
await panelPage.waitForSelector('.toolbar', { state: 'visible' });
82+
83+
const themeAfterReload = await panelPage.evaluate(() =>
84+
document.documentElement.getAttribute('data-theme'),
85+
);
86+
expect(themeAfterReload).toBe(newTheme);
87+
88+
await panelPage.close();
89+
});
90+
91+
test('toggle button survives Elm re-render', async ({ extensionContext, extensionId }) => {
92+
const panelPage = await extensionContext.newPage();
93+
await openPanelPage(panelPage, extensionId);
94+
95+
await expect(panelPage.locator('.theme-toggle')).toBeVisible({ timeout: 3000 });
96+
97+
// Trigger an Elm re-render by injecting an SDK event
98+
await panelPage.evaluate(() => {
99+
chrome.runtime.sendMessage({
100+
type: 'SDK_EVENT',
101+
payload: {
102+
type: 'sdk:node-change',
103+
id: `theme-test-${Date.now()}`,
104+
flowId: 'theme-flow',
105+
timestamp: Date.now(),
106+
source: 'sdk',
107+
causedBy: null,
108+
data: { _tag: 'sdk', nodeStatus: 'continue' },
109+
flags: { isCors: false, isError: false, isAuthRelated: true },
110+
},
111+
});
112+
});
113+
114+
// Wait for the event to appear (forces Elm re-render)
115+
await panelPage.waitForSelector('.tl-row', { state: 'visible', timeout: 5000 });
116+
117+
// Toggle should still be there after Elm re-rendered
118+
await expect(panelPage.locator('.theme-toggle')).toBeVisible({ timeout: 3000 });
119+
120+
await panelPage.close();
121+
});
122+
});

packages/devtools-extension/src/panel/panel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ function initThemeToggle() {
129129
}
130130
});
131131
observer.observe(document.body, { childList: true, subtree: true });
132+
133+
// Eagerly append if the toolbar already exists (Elm.Main.init runs before
134+
// initThemeToggle, so the initial render mutation has already fired).
135+
const toolbar = document.querySelector('.toolbar');
136+
if (toolbar) {
137+
toolbar.appendChild(btn);
138+
}
132139
}
133140

134141
// ── App init ──────────────────────────────────────────────────────────────────

packages/devtools-ui/src/panel.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ details[open] > .jwt-summary::before {
11451145
top: 0;
11461146
left: var(--graph-w);
11471147
width: 5px;
1148-
height: 100%;
1148+
height: calc(100% - var(--insp-h, 220px));
11491149
cursor: col-resize;
11501150
transform: translateX(-2px);
11511151
}

0 commit comments

Comments
 (0)