experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(components): add component test infrastructure

- test-page.html: Test page with all components configured
- components.spec.ts: Playwright tests for all components
- Button: variants, sizes, disabled, loading, click events
- Card: slots, interactive, selected
- List: items, keyboard nav, disabled
- Input: text input, disabled, suggestions
- Select: native and custom modes
- Switch: checked state, toggle, disabled
- Dialog: open/close
- Tabs: selection, panel visibility
- Details: open/close toggle
- Dropdown: trigger, open/close
- Button Group: selection
- Accessibility: ARIA roles and attributes

+809
+553
tests/components/components.spec.ts
··· 1 + /** 2 + * Peek UI Components Tests 3 + * 4 + * Tests for all UI components using Playwright. 5 + * 6 + * Run with: 7 + * npx playwright test tests/components/ 8 + */ 9 + 10 + import { test, expect, Page } from '@playwright/test'; 11 + import path from 'path'; 12 + import { fileURLToPath } from 'url'; 13 + 14 + const __filename = fileURLToPath(import.meta.url); 15 + const __dirname = path.dirname(__filename); 16 + 17 + // Helper to get component's shadow root 18 + async function getShadowRoot(page: Page, selector: string) { 19 + return page.evaluateHandle((sel) => document.querySelector(sel)?.shadowRoot, selector); 20 + } 21 + 22 + // Helper to query inside shadow root 23 + async function queryShadow(page: Page, hostSelector: string, innerSelector: string) { 24 + return page.evaluateHandle( 25 + ({ host, inner }) => document.querySelector(host)?.shadowRoot?.querySelector(inner), 26 + { host: hostSelector, inner: innerSelector } 27 + ); 28 + } 29 + 30 + test.describe('Components @components', () => { 31 + let page: Page; 32 + 33 + test.beforeAll(async ({ browser }) => { 34 + page = await browser.newPage(); 35 + const testPagePath = path.join(__dirname, 'test-page.html'); 36 + await page.goto(`file://${testPagePath}`); 37 + // Wait for components to be ready 38 + await page.waitForSelector('body[data-ready="true"]', { timeout: 10000 }); 39 + }); 40 + 41 + test.afterAll(async () => { 42 + await page.close(); 43 + }); 44 + 45 + // ========================================================================== 46 + // peek-button 47 + // ========================================================================== 48 + 49 + test.describe('peek-button', () => { 50 + test('renders with default variant', async () => { 51 + const btn = page.locator('#btn-default'); 52 + await expect(btn).toBeVisible(); 53 + }); 54 + 55 + test('renders all variants', async () => { 56 + await expect(page.locator('#btn-primary')).toBeVisible(); 57 + await expect(page.locator('#btn-secondary')).toBeVisible(); 58 + await expect(page.locator('#btn-ghost')).toBeVisible(); 59 + await expect(page.locator('#btn-danger')).toBeVisible(); 60 + }); 61 + 62 + test('renders all sizes', async () => { 63 + await expect(page.locator('#btn-sm')).toBeVisible(); 64 + await expect(page.locator('#btn-md')).toBeVisible(); 65 + await expect(page.locator('#btn-lg')).toBeVisible(); 66 + }); 67 + 68 + test('disabled button is not clickable', async () => { 69 + const btn = page.locator('#btn-disabled'); 70 + await expect(btn).toHaveAttribute('disabled', ''); 71 + 72 + // Check that the native button inside shadow DOM is disabled 73 + const isDisabled = await page.evaluate(() => { 74 + const el = document.querySelector('#btn-disabled'); 75 + const innerBtn = el?.shadowRoot?.querySelector('button'); 76 + return innerBtn?.disabled; 77 + }); 78 + expect(isDisabled).toBe(true); 79 + }); 80 + 81 + test('loading button shows spinner', async () => { 82 + const hasSpinner = await page.evaluate(() => { 83 + const el = document.querySelector('#btn-loading'); 84 + const spinner = el?.shadowRoot?.querySelector('.spinner'); 85 + return !!spinner; 86 + }); 87 + expect(hasSpinner).toBe(true); 88 + }); 89 + 90 + test('emits click event', async () => { 91 + const clicked = await page.evaluate(() => { 92 + return new Promise((resolve) => { 93 + const btn = document.querySelector('#btn-primary'); 94 + btn?.addEventListener('click', () => resolve(true), { once: true }); 95 + btn?.shadowRoot?.querySelector('button')?.click(); 96 + }); 97 + }); 98 + expect(clicked).toBe(true); 99 + }); 100 + }); 101 + 102 + // ========================================================================== 103 + // peek-card 104 + // ========================================================================== 105 + 106 + test.describe('peek-card', () => { 107 + test('renders with header, body, and footer', async () => { 108 + const card = page.locator('#card-basic'); 109 + await expect(card).toBeVisible(); 110 + 111 + const content = await page.evaluate(() => { 112 + const card = document.querySelector('#card-basic'); 113 + const header = card?.querySelector('[slot="header"]')?.textContent; 114 + const footer = card?.querySelector('[slot="footer"]')?.textContent; 115 + return { header, footer }; 116 + }); 117 + expect(content.header).toBe('Card Header'); 118 + expect(content.footer).toBe('Card Footer'); 119 + }); 120 + 121 + test('interactive card is focusable', async () => { 122 + const hasTabindex = await page.evaluate(() => { 123 + const card = document.querySelector('#card-interactive'); 124 + return card?.getAttribute('interactive') !== null; 125 + }); 126 + expect(hasTabindex).toBe(true); 127 + }); 128 + 129 + test('selected card has selected attribute', async () => { 130 + await expect(page.locator('#card-selected')).toHaveAttribute('selected', ''); 131 + }); 132 + }); 133 + 134 + // ========================================================================== 135 + // peek-list 136 + // ========================================================================== 137 + 138 + test.describe('peek-list', () => { 139 + test('renders list items', async () => { 140 + const items = page.locator('#list-single peek-list-item'); 141 + await expect(items).toHaveCount(3); 142 + }); 143 + 144 + test('supports keyboard navigation', async () => { 145 + const list = page.locator('#list-single'); 146 + await list.focus(); 147 + await page.keyboard.press('ArrowDown'); 148 + 149 + // First non-disabled item should be focused 150 + const focusedValue = await page.evaluate(() => { 151 + const list = document.querySelector('#list-single') as any; 152 + return list?._focusedIndex; 153 + }); 154 + expect(focusedValue).toBeGreaterThanOrEqual(0); 155 + }); 156 + 157 + test('disabled item cannot be selected', async () => { 158 + const isDisabled = await page.evaluate(() => { 159 + const item = document.querySelector('#list-single peek-list-item[disabled]'); 160 + return item?.hasAttribute('disabled'); 161 + }); 162 + expect(isDisabled).toBe(true); 163 + }); 164 + }); 165 + 166 + // ========================================================================== 167 + // peek-input 168 + // ========================================================================== 169 + 170 + test.describe('peek-input', () => { 171 + test('accepts text input', async () => { 172 + const input = page.locator('#input-basic'); 173 + await input.click(); 174 + 175 + // Type into the shadow DOM input 176 + await page.evaluate(() => { 177 + const el = document.querySelector('#input-basic'); 178 + const innerInput = el?.shadowRoot?.querySelector('input'); 179 + if (innerInput) { 180 + innerInput.value = 'test value'; 181 + innerInput.dispatchEvent(new Event('input', { bubbles: true })); 182 + } 183 + }); 184 + 185 + const value = await page.evaluate(() => { 186 + const el = document.querySelector('#input-basic') as any; 187 + return el?.value; 188 + }); 189 + expect(value).toBe('test value'); 190 + }); 191 + 192 + test('disabled input is not editable', async () => { 193 + const isDisabled = await page.evaluate(() => { 194 + const el = document.querySelector('#input-disabled'); 195 + const innerInput = el?.shadowRoot?.querySelector('input'); 196 + return innerInput?.disabled; 197 + }); 198 + expect(isDisabled).toBe(true); 199 + }); 200 + 201 + test('shows suggestions when typing', async () => { 202 + await page.evaluate(() => { 203 + const el = document.querySelector('#input-suggestions'); 204 + const innerInput = el?.shadowRoot?.querySelector('input'); 205 + if (innerInput) { 206 + innerInput.value = 'App'; 207 + innerInput.dispatchEvent(new Event('input', { bubbles: true })); 208 + innerInput.dispatchEvent(new Event('focus', { bubbles: true })); 209 + } 210 + }); 211 + 212 + // Wait for suggestions to appear 213 + await page.waitForTimeout(100); 214 + 215 + const hasSuggestions = await page.evaluate(() => { 216 + const el = document.querySelector('#input-suggestions'); 217 + const suggestions = el?.shadowRoot?.querySelector('.suggestions'); 218 + return suggestions?.children.length > 0; 219 + }); 220 + expect(hasSuggestions).toBe(true); 221 + }); 222 + }); 223 + 224 + // ========================================================================== 225 + // peek-select 226 + // ========================================================================== 227 + 228 + test.describe('peek-select', () => { 229 + test('native mode renders select element', async () => { 230 + const hasSelect = await page.evaluate(() => { 231 + const el = document.querySelector('#select-native'); 232 + const select = el?.shadowRoot?.querySelector('select'); 233 + return !!select; 234 + }); 235 + expect(hasSelect).toBe(true); 236 + }); 237 + 238 + test('native select has options', async () => { 239 + const optionCount = await page.evaluate(() => { 240 + const el = document.querySelector('#select-native'); 241 + const select = el?.shadowRoot?.querySelector('select'); 242 + return select?.options.length; 243 + }); 244 + expect(optionCount).toBeGreaterThan(0); 245 + }); 246 + 247 + test('custom mode renders trigger button', async () => { 248 + const hasTrigger = await page.evaluate(() => { 249 + const el = document.querySelector('#select-custom'); 250 + const trigger = el?.shadowRoot?.querySelector('.trigger'); 251 + return !!trigger; 252 + }); 253 + expect(hasTrigger).toBe(true); 254 + }); 255 + }); 256 + 257 + // ========================================================================== 258 + // peek-switch 259 + // ========================================================================== 260 + 261 + test.describe('peek-switch', () => { 262 + test('off by default', async () => { 263 + const checked = await page.evaluate(() => { 264 + const el = document.querySelector('#switch-off') as any; 265 + return el?.checked; 266 + }); 267 + expect(checked).toBe(false); 268 + }); 269 + 270 + test('can be checked by default', async () => { 271 + const checked = await page.evaluate(() => { 272 + const el = document.querySelector('#switch-on') as any; 273 + return el?.checked; 274 + }); 275 + expect(checked).toBe(true); 276 + }); 277 + 278 + test('toggles on click', async () => { 279 + const initialState = await page.evaluate(() => { 280 + const el = document.querySelector('#switch-off') as any; 281 + return el?.checked; 282 + }); 283 + 284 + await page.click('#switch-off'); 285 + 286 + const newState = await page.evaluate(() => { 287 + const el = document.querySelector('#switch-off') as any; 288 + return el?.checked; 289 + }); 290 + 291 + expect(newState).toBe(!initialState); 292 + 293 + // Reset 294 + await page.click('#switch-off'); 295 + }); 296 + 297 + test('disabled switch cannot be toggled', async () => { 298 + const isDisabled = await page.evaluate(() => { 299 + const el = document.querySelector('#switch-disabled'); 300 + return el?.hasAttribute('disabled'); 301 + }); 302 + expect(isDisabled).toBe(true); 303 + }); 304 + }); 305 + 306 + // ========================================================================== 307 + // peek-dialog 308 + // ========================================================================== 309 + 310 + test.describe('peek-dialog', () => { 311 + test('opens when show() is called', async () => { 312 + await page.click('#open-dialog'); 313 + await page.waitForTimeout(100); 314 + 315 + const isOpen = await page.evaluate(() => { 316 + const el = document.querySelector('#test-dialog') as any; 317 + return el?.open; 318 + }); 319 + expect(isOpen).toBe(true); 320 + 321 + // Close for next tests 322 + await page.click('#close-dialog'); 323 + await page.waitForTimeout(100); 324 + }); 325 + 326 + test('closes when close() is called', async () => { 327 + await page.click('#open-dialog'); 328 + await page.waitForTimeout(100); 329 + await page.click('#close-dialog'); 330 + await page.waitForTimeout(100); 331 + 332 + const isOpen = await page.evaluate(() => { 333 + const el = document.querySelector('#test-dialog') as any; 334 + return el?.open; 335 + }); 336 + expect(isOpen).toBe(false); 337 + }); 338 + }); 339 + 340 + // ========================================================================== 341 + // peek-tabs 342 + // ========================================================================== 343 + 344 + test.describe('peek-tabs', () => { 345 + test('first tab is selected by default', async () => { 346 + const selected = await page.evaluate(() => { 347 + const tabs = document.querySelector('#test-tabs') as any; 348 + return tabs?.selected; 349 + }); 350 + expect(selected).toBe(0); 351 + }); 352 + 353 + test('first panel is visible', async () => { 354 + const isHidden = await page.evaluate(() => { 355 + const panel = document.querySelector('#panel-1'); 356 + return panel?.hasAttribute('hidden'); 357 + }); 358 + expect(isHidden).toBe(false); 359 + }); 360 + 361 + test('second panel is hidden', async () => { 362 + const isHidden = await page.evaluate(() => { 363 + const panel = document.querySelector('#panel-2'); 364 + return panel?.hasAttribute('hidden'); 365 + }); 366 + expect(isHidden).toBe(true); 367 + }); 368 + 369 + test('clicking tab changes selection', async () => { 370 + // Click second tab 371 + await page.evaluate(() => { 372 + const tabs = document.querySelectorAll('#test-tabs peek-tab'); 373 + (tabs[1] as any)?.shadowRoot?.querySelector('button')?.click(); 374 + }); 375 + 376 + await page.waitForTimeout(50); 377 + 378 + const selected = await page.evaluate(() => { 379 + const tabs = document.querySelector('#test-tabs') as any; 380 + return tabs?.selected; 381 + }); 382 + expect(selected).toBe(1); 383 + 384 + // Reset to first tab 385 + await page.evaluate(() => { 386 + const tabs = document.querySelector('#test-tabs') as any; 387 + tabs?.select(0); 388 + }); 389 + }); 390 + }); 391 + 392 + // ========================================================================== 393 + // peek-details 394 + // ========================================================================== 395 + 396 + test.describe('peek-details', () => { 397 + test('closed by default', async () => { 398 + const isOpen = await page.evaluate(() => { 399 + const el = document.querySelector('#details-closed') as any; 400 + return el?.open; 401 + }); 402 + expect(isOpen).toBe(false); 403 + }); 404 + 405 + test('can be open by default', async () => { 406 + const isOpen = await page.evaluate(() => { 407 + const el = document.querySelector('#details-open') as any; 408 + return el?.open; 409 + }); 410 + expect(isOpen).toBe(true); 411 + }); 412 + 413 + test('toggles on click', async () => { 414 + await page.evaluate(() => { 415 + const el = document.querySelector('#details-closed'); 416 + const summary = el?.shadowRoot?.querySelector('summary'); 417 + summary?.click(); 418 + }); 419 + 420 + await page.waitForTimeout(50); 421 + 422 + const isOpen = await page.evaluate(() => { 423 + const el = document.querySelector('#details-closed') as any; 424 + return el?.open; 425 + }); 426 + expect(isOpen).toBe(true); 427 + 428 + // Reset 429 + await page.evaluate(() => { 430 + const el = document.querySelector('#details-closed') as any; 431 + el?.hide(); 432 + }); 433 + }); 434 + }); 435 + 436 + // ========================================================================== 437 + // peek-dropdown 438 + // ========================================================================== 439 + 440 + test.describe('peek-dropdown', () => { 441 + test('closed by default', async () => { 442 + const isOpen = await page.evaluate(() => { 443 + const el = document.querySelector('#test-dropdown') as any; 444 + return el?.open; 445 + }); 446 + expect(isOpen).toBe(false); 447 + }); 448 + 449 + test('opens on trigger click', async () => { 450 + await page.evaluate(() => { 451 + const dropdown = document.querySelector('#test-dropdown'); 452 + const trigger = dropdown?.querySelector('[slot="trigger"]'); 453 + (trigger as any)?.click(); 454 + }); 455 + 456 + await page.waitForTimeout(100); 457 + 458 + const isOpen = await page.evaluate(() => { 459 + const el = document.querySelector('#test-dropdown') as any; 460 + return el?.open; 461 + }); 462 + expect(isOpen).toBe(true); 463 + 464 + // Close 465 + await page.evaluate(() => { 466 + const el = document.querySelector('#test-dropdown') as any; 467 + el?.hide(); 468 + }); 469 + }); 470 + }); 471 + 472 + // ========================================================================== 473 + // peek-button-group 474 + // ========================================================================== 475 + 476 + test.describe('peek-button-group', () => { 477 + test('has initial selection', async () => { 478 + const value = await page.evaluate(() => { 479 + const el = document.querySelector('#btn-group-single') as any; 480 + return el?.value; 481 + }); 482 + expect(value).toBe('opt2'); 483 + }); 484 + 485 + test('clicking item changes selection', async () => { 486 + await page.evaluate(() => { 487 + const items = document.querySelectorAll('#btn-group-single peek-button-group-item'); 488 + (items[0] as any)?.shadowRoot?.querySelector('button')?.click(); 489 + }); 490 + 491 + await page.waitForTimeout(50); 492 + 493 + const value = await page.evaluate(() => { 494 + const el = document.querySelector('#btn-group-single') as any; 495 + return el?.value; 496 + }); 497 + expect(value).toBe('opt1'); 498 + 499 + // Reset 500 + await page.evaluate(() => { 501 + const el = document.querySelector('#btn-group-single') as any; 502 + el.value = 'opt2'; 503 + }); 504 + }); 505 + }); 506 + 507 + // ========================================================================== 508 + // Accessibility Tests 509 + // ========================================================================== 510 + 511 + test.describe('Accessibility', () => { 512 + test('buttons have role="button"', async () => { 513 + const role = await page.evaluate(() => { 514 + const btn = document.querySelector('#btn-primary'); 515 + const inner = btn?.shadowRoot?.querySelector('button'); 516 + return inner?.getAttribute('role') || inner?.tagName.toLowerCase(); 517 + }); 518 + expect(role).toBe('button'); 519 + }); 520 + 521 + test('list has proper ARIA attributes', async () => { 522 + const hasRole = await page.evaluate(() => { 523 + const list = document.querySelector('#list-single'); 524 + const inner = list?.shadowRoot?.querySelector('[role="listbox"]'); 525 + return !!inner; 526 + }); 527 + expect(hasRole).toBe(true); 528 + }); 529 + 530 + test('tabs have proper ARIA attributes', async () => { 531 + const hasTablist = await page.evaluate(() => { 532 + const tabs = document.querySelector('#test-tabs'); 533 + const tablist = tabs?.shadowRoot?.querySelector('[role="tablist"]'); 534 + return !!tablist; 535 + }); 536 + expect(hasTablist).toBe(true); 537 + }); 538 + 539 + test('dialog is modal', async () => { 540 + await page.click('#open-dialog'); 541 + await page.waitForTimeout(100); 542 + 543 + const isModal = await page.evaluate(() => { 544 + const dialog = document.querySelector('#test-dialog'); 545 + const inner = dialog?.shadowRoot?.querySelector('dialog'); 546 + return inner?.hasAttribute('open'); 547 + }); 548 + expect(isModal).toBe(true); 549 + 550 + await page.click('#close-dialog'); 551 + }); 552 + }); 553 + });
+256
tests/components/test-page.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Peek Components Test Page</title> 7 + <style> 8 + :root { 9 + --theme-bg: #ffffff; 10 + --theme-bg-secondary: #fafafa; 11 + --theme-bg-tertiary: #f5f5f5; 12 + --theme-text: #333333; 13 + --theme-text-secondary: #666666; 14 + --theme-text-muted: #999999; 15 + --theme-accent: #007aff; 16 + --theme-border: #e0e0e0; 17 + } 18 + body { 19 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 20 + padding: 20px; 21 + background: var(--theme-bg); 22 + color: var(--theme-text); 23 + } 24 + .test-section { 25 + margin-bottom: 40px; 26 + padding: 20px; 27 + border: 1px solid var(--theme-border); 28 + border-radius: 8px; 29 + } 30 + .test-section h2 { 31 + margin-top: 0; 32 + margin-bottom: 16px; 33 + font-size: 18px; 34 + } 35 + .test-row { 36 + display: flex; 37 + gap: 12px; 38 + align-items: center; 39 + margin-bottom: 12px; 40 + } 41 + </style> 42 + </head> 43 + <body> 44 + <h1>Peek Components Test Page</h1> 45 + 46 + <!-- Button Tests --> 47 + <section class="test-section" id="button-tests"> 48 + <h2>peek-button</h2> 49 + <div class="test-row"> 50 + <peek-button id="btn-default">Default</peek-button> 51 + <peek-button id="btn-primary" variant="primary">Primary</peek-button> 52 + <peek-button id="btn-secondary" variant="secondary">Secondary</peek-button> 53 + <peek-button id="btn-ghost" variant="ghost">Ghost</peek-button> 54 + <peek-button id="btn-danger" variant="danger">Danger</peek-button> 55 + </div> 56 + <div class="test-row"> 57 + <peek-button id="btn-sm" size="sm">Small</peek-button> 58 + <peek-button id="btn-md" size="md">Medium</peek-button> 59 + <peek-button id="btn-lg" size="lg">Large</peek-button> 60 + </div> 61 + <div class="test-row"> 62 + <peek-button id="btn-disabled" disabled>Disabled</peek-button> 63 + <peek-button id="btn-loading" loading>Loading</peek-button> 64 + </div> 65 + </section> 66 + 67 + <!-- Card Tests --> 68 + <section class="test-section" id="card-tests"> 69 + <h2>peek-card</h2> 70 + <div class="test-row"> 71 + <peek-card id="card-basic"> 72 + <span slot="header">Card Header</span> 73 + <p>Card body content</p> 74 + <span slot="footer">Card Footer</span> 75 + </peek-card> 76 + <peek-card id="card-interactive" interactive> 77 + <span slot="header">Interactive Card</span> 78 + <p>Click me</p> 79 + </peek-card> 80 + <peek-card id="card-selected" selected> 81 + <span slot="header">Selected Card</span> 82 + <p>I'm selected</p> 83 + </peek-card> 84 + </div> 85 + </section> 86 + 87 + <!-- List Tests --> 88 + <section class="test-section" id="list-tests"> 89 + <h2>peek-list</h2> 90 + <peek-list id="list-single" selection="single"> 91 + <peek-list-item value="item1">Item 1</peek-list-item> 92 + <peek-list-item value="item2">Item 2</peek-list-item> 93 + <peek-list-item value="item3" disabled>Item 3 (disabled)</peek-list-item> 94 + </peek-list> 95 + </section> 96 + 97 + <!-- Input Tests --> 98 + <section class="test-section" id="input-tests"> 99 + <h2>peek-input</h2> 100 + <div class="test-row"> 101 + <peek-input id="input-basic" placeholder="Type here..."></peek-input> 102 + <peek-input id="input-disabled" placeholder="Disabled" disabled></peek-input> 103 + </div> 104 + <div class="test-row"> 105 + <peek-input id="input-suggestions" placeholder="Type to filter..."></peek-input> 106 + </div> 107 + </section> 108 + 109 + <!-- Select Tests --> 110 + <section class="test-section" id="select-tests"> 111 + <h2>peek-select</h2> 112 + <div class="test-row"> 113 + <peek-select id="select-native" placeholder="Native select"></peek-select> 114 + <peek-select id="select-custom" mode="custom" placeholder="Custom select"></peek-select> 115 + </div> 116 + </section> 117 + 118 + <!-- Switch Tests --> 119 + <section class="test-section" id="switch-tests"> 120 + <h2>peek-switch</h2> 121 + <div class="test-row"> 122 + <peek-switch id="switch-off">Off by default</peek-switch> 123 + <peek-switch id="switch-on" checked>On by default</peek-switch> 124 + <peek-switch id="switch-disabled" disabled>Disabled</peek-switch> 125 + </div> 126 + </section> 127 + 128 + <!-- Dialog Tests --> 129 + <section class="test-section" id="dialog-tests"> 130 + <h2>peek-dialog</h2> 131 + <peek-button id="open-dialog">Open Dialog</peek-button> 132 + <peek-dialog id="test-dialog"> 133 + <span slot="header">Test Dialog</span> 134 + <p>Dialog content</p> 135 + <div slot="footer"> 136 + <peek-button id="close-dialog" variant="ghost">Close</peek-button> 137 + </div> 138 + </peek-dialog> 139 + </section> 140 + 141 + <!-- Tabs Tests --> 142 + <section class="test-section" id="tabs-tests"> 143 + <h2>peek-tabs</h2> 144 + <peek-tabs id="test-tabs"> 145 + <peek-tab>Tab 1</peek-tab> 146 + <peek-tab>Tab 2</peek-tab> 147 + <peek-tab disabled>Tab 3 (disabled)</peek-tab> 148 + <peek-tab-panel id="panel-1">Panel 1 content</peek-tab-panel> 149 + <peek-tab-panel id="panel-2">Panel 2 content</peek-tab-panel> 150 + <peek-tab-panel id="panel-3">Panel 3 content</peek-tab-panel> 151 + </peek-tabs> 152 + </section> 153 + 154 + <!-- Details Tests --> 155 + <section class="test-section" id="details-tests"> 156 + <h2>peek-details</h2> 157 + <peek-details id="details-closed"> 158 + <span slot="summary">Click to expand</span> 159 + <p>Hidden content</p> 160 + </peek-details> 161 + <peek-details id="details-open" open> 162 + <span slot="summary">Already expanded</span> 163 + <p>Visible content</p> 164 + </peek-details> 165 + </section> 166 + 167 + <!-- Dropdown Tests --> 168 + <section class="test-section" id="dropdown-tests"> 169 + <h2>peek-dropdown</h2> 170 + <peek-dropdown id="test-dropdown"> 171 + <peek-button slot="trigger">Open Dropdown</peek-button> 172 + <peek-dropdown-item value="edit">Edit</peek-dropdown-item> 173 + <peek-dropdown-item value="duplicate">Duplicate</peek-dropdown-item> 174 + <peek-dropdown-divider></peek-dropdown-divider> 175 + <peek-dropdown-item value="delete" danger>Delete</peek-dropdown-item> 176 + </peek-dropdown> 177 + </section> 178 + 179 + <!-- Tooltip Tests --> 180 + <section class="test-section" id="tooltip-tests"> 181 + <h2>peek-tooltip</h2> 182 + <div class="test-row"> 183 + <peek-tooltip id="tooltip-top" content="Tooltip on top" position="top"> 184 + <peek-button>Top</peek-button> 185 + </peek-tooltip> 186 + <peek-tooltip id="tooltip-bottom" content="Tooltip on bottom" position="bottom"> 187 + <peek-button>Bottom</peek-button> 188 + </peek-tooltip> 189 + </div> 190 + </section> 191 + 192 + <!-- Button Group Tests --> 193 + <section class="test-section" id="button-group-tests"> 194 + <h2>peek-button-group</h2> 195 + <peek-button-group id="btn-group-single" selection="single" value="opt2"> 196 + <peek-button-group-item value="opt1">Option 1</peek-button-group-item> 197 + <peek-button-group-item value="opt2">Option 2</peek-button-group-item> 198 + <peek-button-group-item value="opt3">Option 3</peek-button-group-item> 199 + </peek-button-group> 200 + </section> 201 + 202 + <!-- Drawer Tests --> 203 + <section class="test-section" id="drawer-tests"> 204 + <h2>peek-drawer</h2> 205 + <peek-button id="open-drawer">Open Drawer</peek-button> 206 + <peek-drawer id="test-drawer" position="right"> 207 + <span slot="header">Test Drawer</span> 208 + <p>Drawer content</p> 209 + </peek-drawer> 210 + </section> 211 + 212 + <script type="module"> 213 + // Import all components 214 + import '../../app/components/index.js'; 215 + 216 + // Setup test helpers 217 + window.testHelpers = { 218 + getComponent: (id) => document.getElementById(id), 219 + getShadowRoot: (id) => document.getElementById(id)?.shadowRoot, 220 + queryShadow: (id, selector) => document.getElementById(id)?.shadowRoot?.querySelector(selector), 221 + click: (id) => document.getElementById(id)?.click(), 222 + dispatch: (id, event, detail) => { 223 + document.getElementById(id)?.dispatchEvent(new CustomEvent(event, { detail, bubbles: true })); 224 + } 225 + }; 226 + 227 + // Setup select options 228 + document.getElementById('select-native').options = ['Red', 'Green', 'Blue']; 229 + document.getElementById('select-custom').options = [ 230 + { value: 'sm', label: 'Small' }, 231 + { value: 'md', label: 'Medium' }, 232 + { value: 'lg', label: 'Large' } 233 + ]; 234 + 235 + // Setup input suggestions 236 + document.getElementById('input-suggestions').suggestions = ['Apple', 'Banana', 'Cherry', 'Date']; 237 + 238 + // Dialog handlers 239 + document.getElementById('open-dialog').addEventListener('click', () => { 240 + document.getElementById('test-dialog').show(); 241 + }); 242 + document.getElementById('close-dialog').addEventListener('click', () => { 243 + document.getElementById('test-dialog').close(); 244 + }); 245 + 246 + // Drawer handler 247 + document.getElementById('open-drawer').addEventListener('click', () => { 248 + document.getElementById('test-drawer').show(); 249 + }); 250 + 251 + // Signal ready 252 + window.componentsReady = true; 253 + document.body.dataset.ready = 'true'; 254 + </script> 255 + </body> 256 + </html>