Skip to content

Commit e1c41b9

Browse files
committed
feat: let the sandbox load shared scenarios
1 parent 751cbd6 commit e1c41b9

32 files changed

Lines changed: 404 additions & 168 deletions

package-lock.json

Lines changed: 3 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css-selector-generator-benchmark/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"@types/react": "^19.0.0",
2121
"@types/react-dom": "^19.0.0",
2222
"@vitejs/plugin-react": "^4.3.4",
23-
"typescript": "~5.7.2",
23+
"typescript": "^5.9.3",
2424
"vite": "^6.0.7"
2525
}
2626
}

packages/css-selector-generator-sandbox/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
"dependencies": {
1212
"css-selector-generator": "^3.9.4",
1313
"react": "^19.0.0",
14-
"react-dom": "^19.0.0"
14+
"react-dom": "^19.0.0",
15+
"css-selector-generator-scenarios": "*"
1516
},
1617
"devDependencies": {
1718
"@types/react": "^19.0.0",
1819
"@types/react-dom": "^19.0.0",
1920
"@vitejs/plugin-react": "^4.3.4",
20-
"typescript": "~5.7.2",
21+
"typescript": "^5.9.3",
2122
"vite": "^6.0.7"
2223
}
2324
}

packages/css-selector-generator-sandbox/src/App.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,28 @@ body {
219219
top: -9999px;
220220
visibility: hidden;
221221
}
222+
223+
.scenario-picker {
224+
padding: 0.5rem 0.75rem;
225+
border-bottom: 1px solid var(--border-color, #ddd);
226+
}
227+
228+
.scenario-picker label {
229+
display: flex;
230+
gap: 0.5rem;
231+
align-items: center;
232+
font-size: 0.85rem;
233+
}
234+
235+
.scenario-picker select {
236+
flex: 1;
237+
min-width: 0;
238+
padding: 0.25rem;
239+
font: inherit;
240+
}
241+
242+
.scenario-description {
243+
margin: 0.4rem 0 0;
244+
font-size: 0.8rem;
245+
opacity: 0.75;
246+
}

packages/css-selector-generator-sandbox/src/App.tsx

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
import { useState, useRef, useEffect, useCallback } from 'react';
2-
import { HtmlEditor } from './components/HtmlEditor';
3-
import { SelectorPanel } from './components/SelectorPanel';
4-
import { mapSelectorsToLines, getLineCount, type SelectorWithTiming } from './utils/selectorMapper';
5-
import { formatHtml } from './utils/formatHtml';
1+
import { useState, useEffect } from "react";
2+
import {
3+
createScenarioFrame,
4+
scenarios,
5+
} from "css-selector-generator-scenarios";
6+
import { HtmlEditor } from "./components/HtmlEditor";
7+
import { SelectorPanel } from "./components/SelectorPanel";
8+
import { ScenarioPicker } from "./components/ScenarioPicker";
9+
import {
10+
mapSelectorsToLines,
11+
getLineCount,
12+
type SelectorWithTiming,
13+
} from "./utils/selectorMapper";
14+
import { formatHtml } from "./utils/formatHtml";
15+
16+
// The line mapper pairs source tags with elements found by walking the
17+
// rendered DOM, which cannot see into shadow roots.
18+
const PICKABLE_SCENARIOS = scenarios.filter(
19+
(scenario) => !scenario.tags.includes("shadow-dom"),
20+
);
621

722
const SAMPLE_HTML = `<section class="demo">
823
<article>
@@ -49,35 +64,49 @@ const SAMPLE_HTML = `<section class="demo">
4964

5065
function App() {
5166
const [htmlSource, setHtmlSource] = useState(SAMPLE_HTML);
52-
const [selectorsByLine, setSelectorsByLine] = useState<Map<number, SelectorWithTiming[]>>(new Map());
67+
const [selectorsByLine, setSelectorsByLine] = useState<
68+
Map<number, SelectorWithTiming[]>
69+
>(new Map());
5370
const [totalTimeMs, setTotalTimeMs] = useState(0);
5471
const [showInfoPopover, setShowInfoPopover] = useState(false);
55-
const iframeRef = useRef<HTMLIFrameElement>(null);
72+
const [scenarioId, setScenarioId] = useState("");
5673

57-
const updateSelectors = useCallback(() => {
58-
if (!iframeRef.current?.contentDocument) return;
59-
60-
const doc = iframeRef.current.contentDocument;
61-
62-
// Write HTML to the iframe's document body
63-
doc.body.innerHTML = htmlSource;
64-
65-
// Generate selectors using the iframe's body as root
66-
const result = mapSelectorsToLines(htmlSource, doc.body);
67-
setSelectorsByLine(result.selectorsByLine);
68-
setTotalTimeMs(result.totalTimeMs);
74+
useEffect(() => {
75+
let cancelled = false;
76+
77+
void (async () => {
78+
// Loaded the same way as in the library's tests, so that the document
79+
// shape matches. Selectors can still differ from a scenario's stated
80+
// expectation, because everything here is generated against the body.
81+
const iframe = await createScenarioFrame(htmlSource, document);
82+
try {
83+
const doc = iframe.contentDocument;
84+
if (!doc || cancelled) return;
85+
const result = mapSelectorsToLines(htmlSource, doc.body);
86+
setSelectorsByLine(result.selectorsByLine);
87+
setTotalTimeMs(result.totalTimeMs);
88+
} finally {
89+
iframe.remove();
90+
}
91+
})();
92+
93+
return () => {
94+
cancelled = true;
95+
};
6996
}, [htmlSource]);
7097

71-
useEffect(() => {
72-
updateSelectors();
73-
}, [updateSelectors]);
98+
const handleScenarioChange = (id: string) => {
99+
setScenarioId(id);
100+
const scenario = PICKABLE_SCENARIOS.find((item) => item.id === id);
101+
setHtmlSource(scenario ? scenario.html.trim() : SAMPLE_HTML);
102+
};
74103

75104
const lineCount = getLineCount(htmlSource);
76105

77106
// Calculate total element count
78107
const elementCount = Array.from(selectorsByLine.values()).reduce(
79108
(total, selectors) => total + selectors.length,
80-
0
109+
0,
81110
);
82111

83112
const handleFormat = () => {
@@ -89,17 +118,27 @@ function App() {
89118
<div className="app-title">
90119
<h1>CSS Selector Generator Sandbox</h1>
91120
<p>
92-
A playground for testing{' '}
93-
<a href="https://github.com/fczbkk/css-selector-generator#readme">css-selector-generator</a>,
94-
a JavaScript library that generates unique CSS selectors for any HTML element.
121+
A playground for testing{" "}
122+
<a href="https://github.com/fczbkk/css-selector-generator#readme">
123+
css-selector-generator
124+
</a>
125+
, a JavaScript library that generates unique CSS selectors for any
126+
HTML element.
95127
</p>
96128
</div>
97129
<div className="panels-content">
98130
<div className="panel-column">
99131
<div className="panel-header">
100132
HTML
101-
<button className="format-btn" onClick={handleFormat}>Format</button>
133+
<button className="format-btn" onClick={handleFormat}>
134+
Format
135+
</button>
102136
</div>
137+
<ScenarioPicker
138+
scenarios={PICKABLE_SCENARIOS}
139+
value={scenarioId}
140+
onChange={handleScenarioChange}
141+
/>
103142
<HtmlEditor value={htmlSource} onChange={setHtmlSource} />
104143
</div>
105144
<div className="panel-column">
@@ -116,15 +155,20 @@ function App() {
116155
{showInfoPopover && (
117156
<div className="info-popover">
118157
<p>Total time to generate all CSS selectors.</p>
119-
<p>Hover over individual selectors to see their generation time.</p>
158+
<p>
159+
Hover over individual selectors to see their generation
160+
time.
161+
</p>
120162
</div>
121163
)}
122164
</span>
123165
</div>
124-
<SelectorPanel selectorsByLine={selectorsByLine} lineCount={lineCount} />
166+
<SelectorPanel
167+
selectorsByLine={selectorsByLine}
168+
lineCount={lineCount}
169+
/>
125170
</div>
126171
</div>
127-
<iframe ref={iframeRef} className="hidden-container" title="HTML Renderer" />
128172
</div>
129173
);
130174
}

packages/css-selector-generator-sandbox/src/components/HtmlEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from 'react';
1+
import { useEffect, useRef } from "react";
22

33
interface HtmlEditorProps {
44
value: string;
@@ -13,7 +13,7 @@ export function HtmlEditor({ value, onChange }: HtmlEditorProps) {
1313
if (!textarea) return;
1414

1515
// Reset height to auto to get the correct scrollHeight
16-
textarea.style.height = 'auto';
16+
textarea.style.height = "auto";
1717
// Set height to scrollHeight to fit content
1818
textarea.style.height = `${textarea.scrollHeight}px`;
1919
}, [value]);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Scenario } from "css-selector-generator-scenarios";
2+
3+
interface ScenarioPickerProps {
4+
scenarios: Scenario[];
5+
value: string;
6+
onChange: (id: string) => void;
7+
}
8+
9+
export function ScenarioPicker({
10+
scenarios,
11+
value,
12+
onChange,
13+
}: ScenarioPickerProps) {
14+
const selected = scenarios.find((scenario) => scenario.id === value);
15+
16+
return (
17+
<div className="scenario-picker">
18+
<label>
19+
Scenario
20+
<select
21+
value={value}
22+
onChange={(event) => onChange(event.target.value)}
23+
>
24+
<option value="">Sample</option>
25+
{scenarios.map((scenario) => (
26+
<option key={scenario.id} value={scenario.id}>
27+
{scenario.title ?? scenario.id}
28+
</option>
29+
))}
30+
</select>
31+
</label>
32+
{selected?.description && (
33+
<p className="scenario-description">{selected.description}</p>
34+
)}
35+
</div>
36+
);
37+
}

packages/css-selector-generator-sandbox/src/components/SelectorPanel.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
import type { SelectorWithTiming } from '../utils/selectorMapper';
1+
import type { SelectorWithTiming } from "../utils/selectorMapper";
22

33
interface SelectorPanelProps {
44
selectorsByLine: Map<number, SelectorWithTiming[]>;
55
lineCount: number;
66
}
77

8-
export function SelectorPanel({ selectorsByLine, lineCount }: SelectorPanelProps) {
8+
export function SelectorPanel({
9+
selectorsByLine,
10+
lineCount,
11+
}: SelectorPanelProps) {
912
const lines = [];
1013

1114
for (let i = 0; i < lineCount; i++) {
1215
const selectors = selectorsByLine.get(i) || [];
1316
lines.push(
1417
<div key={i} className="selector-line">
1518
{selectors.map((s, idx) => (
16-
<span key={idx} className="selector" title={`${s.timeMs.toFixed(3)} ms`}>
19+
<span
20+
key={idx}
21+
className="selector"
22+
title={`${s.timeMs.toFixed(3)} ms`}
23+
>
1724
{s.selector}
18-
{idx < selectors.length - 1 ? ', ' : ''}
25+
{idx < selectors.length - 1 ? ", " : ""}
1926
</span>
2027
))}
21-
</div>
28+
</div>,
2229
);
2330
}
2431

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { StrictMode } from 'react'
2-
import { createRoot } from 'react-dom/client'
3-
import './App.css'
4-
import App from './App.tsx'
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
import "./App.css";
4+
import App from "./App.tsx";
55

6-
createRoot(document.getElementById('root')!).render(
6+
createRoot(document.getElementById("root")!).render(
77
<StrictMode>
88
<App />
99
</StrictMode>,
10-
)
10+
);

packages/css-selector-generator-sandbox/src/utils/selectorMapper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ function parseOpeningTags(htmlSource: string): TagInfo[] {
6767
// Get all elements from a container in document order
6868
function getAllElements(container: Element): Element[] {
6969
const elements: Element[] = [];
70-
const walker = document.createTreeWalker(
70+
// The container lives in the scenario iframe, not in this document.
71+
const walker = container.ownerDocument.createTreeWalker(
7172
container,
7273
NodeFilter.SHOW_ELEMENT,
7374
null

0 commit comments

Comments
 (0)