diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml new file mode 100644 index 00000000..7e3f8939 --- /dev/null +++ b/.github/workflows/ci-docs.yml @@ -0,0 +1,33 @@ +name: Continuous Integration (Docs) + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node: [22] + + steps: + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + - name: Installing project dependencies + run: | + npm ci + - name: Setup Playwright + run: | + npx playwright install --with-deps + - name: Build the website + run: | + npm run build + - name: Check Types + run: | + npm run check:types + - name: Test Docs + run: | + npm run test:docs diff --git a/.github/workflows/ci-win.yml b/.github/workflows/ci-win.yml index 9e45999a..234a1320 100644 --- a/.github/workflows/ci-win.yml +++ b/.github/workflows/ci-win.yml @@ -21,4 +21,4 @@ jobs: npm ci - name: Test run: | - npm test + npm run test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44905bee..9faf5fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,6 @@ jobs: - name: Lint run: | npm run lint - - name: Check Types - run: | - npm run check:types - name: Test run: | - npm test + npm run test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0344567f..f71957b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ To develop for the project, you'll want to follow these steps: 1. Clone the repository 1. Have [NodeJS LTS](https://nodejs.org) installed and / or use `nvm` (see below) 1. Run `npm ci` +1. Run `npx playwright install` ### NVM @@ -48,6 +49,11 @@ The website is built with [**Greenwood**](https://www.greenwoodjs.dev). To run t - `npm run build` - Generate a production build - `npm run serve` - Serve a production build +To run tests for the website, first make sure you have at least run `npm run build`, then: + +- `npm run test:docs` - Run a single suite of tests with Vitest +- `npm run test:docs:tdd` - Run the test suite with Vitest in watch mode + ### Sandbox To assist in local development of WCC, there is a "sandbox" app built into the website, that can be used to validate a number of examples in the browser. (think of it as a storybook for WCC). diff --git a/docs/components/banner-cta/banner-cta.test.ts b/docs/components/banner-cta/banner-cta.test.ts new file mode 100644 index 00000000..4ca1ecdd --- /dev/null +++ b/docs/components/banner-cta/banner-cta.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './banner-cta.tsx'; + +describe('Components/Banner CTA', () => { + let bannerCta: HTMLElement; + + describe('Default Behavior', () => { + const cta = 'npm i -D wc-compiler'; + + beforeEach(() => { + bannerCta = document.createElement('wcc-banner-cta'); + + document.body.appendChild(bannerCta); + }); + + it('should not be undefined', () => { + expect(bannerCta).not.equal(undefined); + }); + + it('should have the main overview text', () => { + const paragraph = bannerCta.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent.trim().replace(/\s+/g, ' ')).toContain( + 'WCC (WC Compiler) is a NodeJS based package for server-rendering native Web Components.', + ); + }); + + it('should have the copy to clipboard snippet', () => { + const pre = bannerCta.querySelectorAll('pre'); + + expect(pre.length).equal(1); + expect(pre[0].textContent.trim()).equal(`$ ${cta}`); + }); + + it('should have the copy to clipboard component', () => { + const ctcButton = bannerCta.querySelectorAll('wcc-ctc-button'); + + expect(ctcButton.length).equal(1); + expect(ctcButton[0].getAttribute('content')).equal(cta); + }); + }); + + afterEach(() => { + bannerCta.remove(); + bannerCta = undefined; + }); +}); diff --git a/docs/components/banner-splash/banner-splash.test.ts b/docs/components/banner-splash/banner-splash.test.ts new file mode 100644 index 00000000..b5ad7a2b --- /dev/null +++ b/docs/components/banner-splash/banner-splash.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './banner-splash.tsx'; + +describe('Components/Banner Splash', () => { + let bannerSplash: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + bannerSplash = document.createElement('wcc-banner-splash'); + + document.body.appendChild(bannerSplash); + }); + + it('should not be undefined', () => { + expect(bannerSplash).not.equal(undefined); + }); + + it('should have to the expected heading text', () => { + const heading = bannerSplash.querySelectorAll('h1'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim().replace(/\s+/g, ' ')).equal('SSR for Web Components'); + }); + }); + + afterEach(() => { + bannerSplash.remove(); + bannerSplash = undefined; + }); +}); diff --git a/docs/components/capability-box/capability-box.test.ts b/docs/components/capability-box/capability-box.test.ts new file mode 100644 index 00000000..f335899b --- /dev/null +++ b/docs/components/capability-box/capability-box.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './capability-box.tsx'; + +describe('Components/Capability Box', () => { + let capabilityBox: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + capabilityBox = document.createElement('wcc-capability-box'); + + document.body.appendChild(capabilityBox); + }); + + it('should not be undefined', () => { + expect(capabilityBox).not.equal(undefined); + }); + + it('should not have any heading text', () => { + const heading = capabilityBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent).equal(''); + }); + }); + + describe('Custom Text and Heading', () => { + const customHeading = 'JSX'; + const customContent = 'This is a custom feature box'; + + beforeEach(() => { + capabilityBox = document.createElement('wcc-capability-box'); + capabilityBox.innerHTML = `

${customContent}

`; + capabilityBox.setAttribute('heading', customHeading); + + document.body.appendChild(capabilityBox); + }); + + it('should not be undefined', () => { + expect(capabilityBox).not.equal(undefined); + }); + + it('should have the expected text from setting the heading attribute', () => { + const heading = capabilityBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent).equal(customHeading); + }); + + it('should have the expected inner HTML text', () => { + const paragraph = capabilityBox.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent).equal(customContent); + }); + }); + + afterEach(() => { + capabilityBox.remove(); + capabilityBox = undefined; + }); +}); diff --git a/docs/components/capability-box/capability-box.tsx b/docs/components/capability-box/capability-box.tsx index 7d56cf51..d598130f 100644 --- a/docs/components/capability-box/capability-box.tsx +++ b/docs/components/capability-box/capability-box.tsx @@ -6,12 +6,12 @@ export default class CapabilityBox extends HTMLElement { } render() { - const heading = this.getAttribute('heading'); + const heading = this.getAttribute('heading') ?? ''; const { innerHTML } = this; return (
- {heading} +

{heading}

{innerHTML}
); diff --git a/docs/components/ctc-button/ctc-button.test.ts b/docs/components/ctc-button/ctc-button.test.ts new file mode 100644 index 00000000..b2cd9aee --- /dev/null +++ b/docs/components/ctc-button/ctc-button.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './ctc-button.tsx'; + +describe('Components/Copy To Clipboard (Button)', () => { + const content = 'npm i -D wc-compiler'; + let ctc: HTMLElement; + + beforeEach(async () => { + ctc = document.createElement('wcc-ctc-button'); + + ctc.setAttribute('content', content); + document.body.appendChild(ctc); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(ctc).not.equal(undefined); + }); + + it('should have an icon with the user provided content set', () => { + const icon = ctc.shadowRoot.querySelectorAll("[title='Copy to clipboard']"); + + expect(icon.length).to.equal(1); + }); + }); + + afterEach(() => { + ctc.remove(); + ctc = undefined; + }); +}); diff --git a/docs/components/feature-box/feature-box.test.ts b/docs/components/feature-box/feature-box.test.ts new file mode 100644 index 00000000..afff6f72 --- /dev/null +++ b/docs/components/feature-box/feature-box.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './feature-box.tsx'; + +describe('Components/Feature Box', () => { + let featureBox: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(() => { + featureBox = document.createElement('wcc-feature-box'); + + document.body.appendChild(featureBox); + }); + + it('should not be undefined', () => { + expect(featureBox).not.equal(undefined); + }); + + it('should not have any heading text', () => { + const heading = featureBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim()).equal(''); + }); + + it('should not have an SVG mapped logo', () => { + const logo = featureBox.querySelectorAll('svg'); + + expect(logo.length).equal(0); + }); + }); + + describe('Custom Text and Heading', () => { + const customHeading = 'JSX'; + const customContent = 'This is a custom feature box'; + + beforeEach(() => { + featureBox = document.createElement('wcc-feature-box'); + featureBox.innerHTML = `

${customContent}

`; + featureBox.setAttribute('heading', customHeading); + + document.body.appendChild(featureBox); + }); + + it('should not be undefined', () => { + expect(featureBox).not.equal(undefined); + }); + + it('should have the expected text from setting the heading attribute', () => { + const heading = featureBox.querySelectorAll('h4'); + + expect(heading.length).equal(1); + expect(heading[0].textContent.trim()).equal(customHeading); + }); + + it('should have the expected inner HTML text', () => { + const paragraph = featureBox.querySelectorAll('p'); + + expect(paragraph.length).equal(1); + expect(paragraph[0].textContent).equal(customContent); + }); + + it('should not a mapped SVG logo', () => { + const logo = featureBox.querySelectorAll('svg'); + + expect(logo.length).equal(1); + }); + }); + + afterEach(() => { + featureBox.remove(); + featureBox = undefined; + }); +}); diff --git a/docs/components/feature-box/feature-box.tsx b/docs/components/feature-box/feature-box.tsx index 7cb42e4b..0c920436 100644 --- a/docs/components/feature-box/feature-box.tsx +++ b/docs/components/feature-box/feature-box.tsx @@ -15,16 +15,16 @@ export default class FeatureBox extends HTMLElement { } render() { - const heading = this.getAttribute('heading'); + const heading = this.getAttribute('heading') ?? ''; const { innerHTML } = this; - const icon = FeatureBox.ICON_MAPPER[heading]; + const icon = FeatureBox.ICON_MAPPER[heading] ?? ''; return (
- +

{icon} {heading} - +

{innerHTML}
); diff --git a/docs/components/footer/footer.test.ts b/docs/components/footer/footer.test.ts new file mode 100644 index 00000000..943af884 --- /dev/null +++ b/docs/components/footer/footer.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './footer.tsx'; + +describe('Components/Footer', () => { + let footer: HTMLElement; + + describe('Default Behavior', () => { + beforeEach(async () => { + footer = document.createElement('wcc-footer'); + + document.body.appendChild(footer); + }); + + it('should not be undefined', () => { + expect(footer).not.equal(undefined); + expect(footer.querySelectorAll('footer').length).equal(1); + }); + + it('should have a link for to the home page', () => { + const homeLink = footer.querySelectorAll('a[title="WCC Home Page"]'); + + expect(homeLink.length).equal(1); + expect(homeLink[0].getAttribute('href')).equal('/'); + }); + + it('should have the WCC logo inside the home page link', () => { + const logo = footer.querySelectorAll('a[title="WCC Home Page"] > svg'); + + expect(logo.length).equal(1); + }); + + it('should have one instance of the social tray component', () => { + const socialTray = footer.querySelectorAll('wcc-social-tray'); + + expect(socialTray.length).equal(1); + }); + }); + + afterEach(() => { + footer.remove(); + footer = undefined; + }); +}); diff --git a/docs/components/header/header.test.ts b/docs/components/header/header.test.ts new file mode 100644 index 00000000..8acf8c93 --- /dev/null +++ b/docs/components/header/header.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect, beforeEach, afterEach, vi, beforeAll, afterAll } from 'vitest'; +import pages from '../../../.greenwood/graph.json' with { type: 'json' }; +import type { Page } from '@greenwood/cli'; +import './header.tsx'; + +const CURRENT_ROUTE = '/docs/'; + +describe('Components/Header', () => { + let header: HTMLElement; + + beforeAll(() => { + window.fetch = vi.fn((): Promise => { + return new Promise((resolve) => { + resolve( + new Response(JSON.stringify(pages.filter((page) => page.data.collection === 'nav'))), + ); + }); + }); + }); + + beforeEach(async () => { + header = document.createElement('wcc-header'); + header.setAttribute('current-route', CURRENT_ROUTE); + + document.body.appendChild(header); + + // to support async connected callback usage + await vi.waitUntil(() => header.querySelector('header')); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(header).not.equal(undefined); + expect(header.querySelectorAll('header').length).equal(1); + }); + + it('should have an anchor tag with title attribute wrapping the logo', () => { + const anchor = header.querySelector("a[title='WCC Home Page']"); + + expect(anchor).to.not.equal(undefined); + expect(anchor.getAttribute('href')).to.equal('/'); + }); + + it('should have the WCC logo', () => { + const logo = header.querySelectorAll("a[title='WCC Home Page'] svg"); + + expect(logo.length).equal(1); + expect(logo[0]).not.equal(undefined); + }); + + it('should have the expected desktop navigation links', () => { + const links = header.querySelectorAll("nav[aria-label='Main'] ul li a"); + let activeRoute: Page; + + Array.from(links).forEach((link, idx) => { + const navItem = pages.find((nav) => nav.route === link.getAttribute('href')); + + expect(navItem).to.not.equal(undefined); + expect(navItem.data.order).to.equal((idx += 1)); + expect(link.textContent).to.equal(navItem.label); + + // Home page doesn't have a title, for example + // maybe a Greenwood bug? + if (navItem.title) { + expect(link.getAttribute('title')).to.equal(navItem.title); + } + + // current route should display as active + if (navItem.route === CURRENT_ROUTE && link.getAttribute('class').includes('active')) { + activeRoute = navItem; + } + }); + + expect(activeRoute.route).to.equal(CURRENT_ROUTE); + }); + }); + + describe('Mobile Menu', () => { + const popoverTarget = 'mobile-menu'; + + it('should have the expected mobile menu icon button', () => { + const mobileIconButton = header.querySelectorAll( + "button[aria-label='Mobile Menu Icon Button']", + ); + + expect(mobileIconButton.length).to.equal(1); + expect(mobileIconButton[0].getAttribute('popovertarget')).to.equal(popoverTarget); + }); + + it('should have the expected popover overlay container', () => { + const overlay = header.querySelectorAll(`#${popoverTarget}`); + + expect(overlay.length).to.equal(1); + expect(overlay[0].getAttribute('popover')).to.equal('manual'); + }); + + it('should have the expected close button', () => { + const mobileCloseButton = header.querySelectorAll( + "button[aria-label='Mobile Menu Close Button']", + ); + + expect(mobileCloseButton.length).to.equal(1); + expect(mobileCloseButton[0].getAttribute('popovertarget')).to.equal(popoverTarget); + expect(mobileCloseButton[0].getAttribute('popovertargetaction')).to.equal('hide'); + }); + + it('should have the expected navigation links', () => { + const links = header.querySelectorAll("nav[aria-label='Mobile'] ul li a"); + let activeRoute: Page = undefined; + + Array.from(links).forEach((link, idx) => { + const navItem = pages.find((nav) => nav.route === link.getAttribute('href')); + + expect(navItem).to.not.equal(undefined); + expect(navItem.data.order).to.equal((idx += 1)); + expect(link.textContent).to.equal(navItem.label); + + // Home page doesn't have a title, for example + // maybe a Greenwood bug? + if (navItem.title) { + expect(link.getAttribute('title')).to.equal(navItem.title); + } + + // current route should display as active + if (navItem.route === CURRENT_ROUTE && link.getAttribute('class').includes('active')) { + activeRoute = navItem; + } + }); + + expect(activeRoute.route).to.equal(CURRENT_ROUTE); + }); + }); + + afterEach(() => { + header.remove(); + header = undefined; + }); + + afterAll(() => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }); +}); diff --git a/docs/components/sidenav/sidenav.module.css b/docs/components/side-nav/side-nav.module.css similarity index 100% rename from docs/components/sidenav/sidenav.module.css rename to docs/components/side-nav/side-nav.module.css diff --git a/docs/components/side-nav/side-nav.test.ts b/docs/components/side-nav/side-nav.test.ts new file mode 100644 index 00000000..b27252e6 --- /dev/null +++ b/docs/components/side-nav/side-nav.test.ts @@ -0,0 +1,207 @@ +import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest'; +import type { DocsPage } from './side-nav.tsx'; +import './side-nav.tsx'; + +const ROUTE = '/docs/'; +const HEADING = 'Docs'; + +// have to hardcode this since tableOfContents data is not set in the graph +const GRAPH = [ + { + id: 'docs', + label: 'Docs', + title: 'Docs', + route: '/docs/', + layout: 'docs', + data: { + collection: 'nav', + order: 2, + tocHeading: 2, + tableOfContents: [ + { + content: 'API', + slug: 'api', + lvl: 2, + i: 1, + seen: 0, + }, + { + content: 'Metadata', + slug: 'metadata', + lvl: 2, + i: 4, + seen: 0, + }, + { + content: 'Progressive Hydration', + slug: 'progressive-hydration', + lvl: 2, + i: 5, + seen: 0, + }, + { + content: 'Data', + slug: 'data', + lvl: 2, + i: 6, + seen: 0, + }, + { + content: 'Conventions', + slug: 'conventions', + lvl: 2, + i: 9, + seen: 0, + }, + { + content: 'TypeScript', + slug: 'typescript', + lvl: 2, + i: 10, + seen: 0, + }, + { + content: 'JSX', + slug: 'jsx', + lvl: 2, + i: 12, + seen: 0, + }, + ], + }, + }, +]; + +describe('Components/Side Nav', () => { + let nav: HTMLElement; + let expectedDocsContent: DocsPage; + + beforeAll(() => { + window.fetch = vi.fn((): Promise => { + return new Promise((resolve) => { + resolve(new Response(JSON.stringify(GRAPH))); + }); + }); + }); + + beforeEach(async () => { + nav = document.createElement('wcc-side-nav'); + nav.setAttribute('route', ROUTE); + nav.setAttribute('heading', HEADING); + + expectedDocsContent = GRAPH.find((page) => page.route === ROUTE); + + document.body.appendChild(nav); + + // to support async connected callback usage + await vi.waitUntil(() => nav.querySelector('nav')); + }); + + describe('Default Behavior - Main Menu', () => { + let fullMenu: HTMLElement; + + beforeEach(async () => { + fullMenu = nav.querySelector('#main-menu'); + }); + + it('should not be null', () => { + expect(fullMenu).not.equal(undefined); + }); + + it('should have the expected ToC heading', () => { + const heading = fullMenu.querySelectorAll('p'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal('Table of Contents'); + }); + + it('should have the expected number of section heading links', () => { + const links = fullMenu.querySelectorAll('ul li a'); + + expect(links.length).to.equal(expectedDocsContent.data.tableOfContents.length); + }); + + it('should have the expected content for section headings links', () => { + const links = fullMenu.querySelectorAll('ul li a'); + + links.forEach((link, index) => { + expect(link.textContent).to.equal(expectedDocsContent.data.tableOfContents[index].content); + }); + }); + + afterAll(() => { + fullMenu = null; + }); + }); + + describe('Default Behavior - Mobile Menu', () => { + let compactMenu: HTMLElement; + let popoverSelector = 'compact-menu'; + + beforeEach(async () => { + compactMenu = nav.querySelector(`#mobile-menu`); + }); + + it('should not be null', () => { + expect(compactMenu).not.equal(undefined); + }); + + it('should have the expected ToC heading', () => { + const heading = compactMenu.querySelectorAll('p'); + + expect(heading.length).to.equal(1); + expect(heading[0].textContent).to.equal('Table of Contents'); + }); + + it('should have the expected popover trigger element', () => { + const trigger = compactMenu.querySelectorAll( + `[popovertarget="${popoverSelector}"]:not([popovertargetaction])`, + ); + + expect(trigger.length).to.equal(1); + expect(trigger[0].textContent).to.contain(HEADING); + }); + + it('should have the expected popover element', () => { + const popover = compactMenu.querySelectorAll(`[popover="manual"]`); + + expect(popover.length).to.equal(1); + }); + + it('should have the expected popover close button', () => { + const closeButton = compactMenu.querySelectorAll( + `[popover="manual"] [popovertarget="${popoverSelector}"]`, + ); + + expect(closeButton.length).to.equal(1); + }); + + it('should have the expected number of section heading links', () => { + const links = compactMenu.querySelectorAll('ul li a'); + + expect(links.length).to.equal(expectedDocsContent.data.tableOfContents.length); + }); + + it('should have the expected content for section headings links', () => { + const links = compactMenu.querySelectorAll('ul li a'); + + links.forEach((link, index) => { + expect(link.textContent).to.equal(expectedDocsContent.data.tableOfContents[index].content); + }); + }); + + afterEach(() => { + compactMenu = null; + }); + }); + + afterEach(() => { + nav.remove(); + nav = undefined; + }); + + afterAll(() => { + vi.clearAllMocks(); + vi.resetAllMocks(); + }); +}); diff --git a/docs/components/sidenav/sidenav.tsx b/docs/components/side-nav/side-nav.tsx similarity index 85% rename from docs/components/sidenav/sidenav.tsx rename to docs/components/side-nav/side-nav.tsx index 6d820cae..109055ed 100644 --- a/docs/components/sidenav/sidenav.tsx +++ b/docs/components/side-nav/side-nav.tsx @@ -1,13 +1,13 @@ import type { Page, Graph } from '@greenwood/cli'; import { getContent } from '@greenwood/cli/src/data/client.js'; -import styles from './sidenav.module.css'; +import styles from './side-nav.module.css'; -type TableOfContents = Array<{ +export type TableOfContents = Array<{ content: string; slug: string; }>; -type DocsPage = Page & { +export type DocsPage = Page & { data?: { tableOfContents?: TableOfContents; }; @@ -38,13 +38,13 @@ export default class SideNav extends HTMLElement { .join(''); return ( -
-
+
+ ); } } -customElements.define('wcc-sidenav', SideNav); +customElements.define('wcc-side-nav', SideNav); diff --git a/docs/components/social-tray/social-tray.test.ts b/docs/components/social-tray/social-tray.test.ts new file mode 100644 index 00000000..589d60bd --- /dev/null +++ b/docs/components/social-tray/social-tray.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import './social-tray.tsx'; + +const ICONS = [ + { + link: 'https://github.com/ProjectEvergreen/wcc', + title: 'GitHub', + }, + { + link: 'https://www.greenwoodjs.dev/discord/', + title: 'Discord', + }, + { + link: 'https://bsky.app/profile/projectevergreen.bsky.social', + title: 'BlueSky', + }, + { + link: 'https://twitter.com/PrjEvergreen', + title: 'Twitter', + }, +]; + +describe('Components/Social Tray', () => { + let tray: HTMLElement; + + beforeEach(async () => { + tray = document.createElement('wcc-social-tray'); + + document.body.appendChild(tray); + }); + + describe('Default Behavior', () => { + it('should not be null', () => { + expect(tray).not.equal(undefined); + expect(tray.querySelectorAll('ul').length).equal(1); + }); + + it('should have the expected social link icons', () => { + const links = tray.querySelectorAll('ul li a'); + const icons = tray.querySelectorAll('ul li a svg'); + const noShowScreenReader = tray.querySelectorAll('ul li a span.no-show-screen-reader'); + + expect(links.length).to.equal(4); + expect(icons.length).to.equal(4); + expect(noShowScreenReader.length).to.equal(4); + + Array.from(links).forEach((link) => { + const iconItem = ICONS.find((icon) => icon.title === link.getAttribute('title')); + + expect(iconItem).to.not.equal(undefined); + expect(link.getAttribute('href')).to.equal(iconItem.link); + expect(link.getAttribute('target')).equal('_blank'); + }); + }); + }); + + afterEach(() => { + tray.remove(); + tray = undefined; + }); +}); diff --git a/docs/layouts/docs.html b/docs/layouts/docs.html index 0a3b74c8..3b1ff7d9 100644 --- a/docs/layouts/docs.html +++ b/docs/layouts/docs.html @@ -2,11 +2,11 @@ WCC - ${globalThis.page.title} - + - +
diff --git a/docs/layouts/examples.html b/docs/layouts/examples.html index e8c71e05..7ffe8d33 100644 --- a/docs/layouts/examples.html +++ b/docs/layouts/examples.html @@ -2,12 +2,11 @@ WCC - ${globalThis.page.title} - + - - +
diff --git a/docs/styles/docs.css b/docs/styles/docs.css index 9fc60d51..cedf0b99 100644 --- a/docs/styles/docs.css +++ b/docs/styles/docs.css @@ -1,4 +1,4 @@ -wcc-sidenav { +wcc-side-nav { display: block; margin: var(--size-4) auto; } @@ -89,7 +89,7 @@ body:has(#compact-menu:popover-open) { font-size: var(--font-size-1); } - wcc-sidenav { + wcc-side-nav { display: inline-block; width: 20%; min-width: 25%; diff --git a/package-lock.json b/package-lock.json index 682b3691..17d0a40f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,8 @@ "@mapbox/rehype-prism": "^0.8.0", "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", + "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", @@ -40,6 +42,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", "prismjs": "^1.28.0", @@ -51,7 +54,9 @@ "stylelint": "^16.10.0", "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.2", + "vite": "^7.3.1", + "vitest": "^4.0.18" }, "engines": { "node": ">=18" @@ -79,6 +84,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -89,6 +104,36 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -274,6 +319,448 @@ "url": "https://github.com/sponsors/JounQin" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -761,6 +1248,13 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@projectevergreen/acorn-jsx-esm": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@projectevergreen/acorn-jsx-esm/-/acorn-jsx-esm-0.1.0.tgz", @@ -1228,6 +1722,13 @@ "node": ">=6" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -1272,6 +1773,27 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/chai/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/co-body": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", @@ -1323,6 +1845,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1772,23 +2301,242 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", "dev": true, - "license": "ISC" + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/abab": { "version": "2.0.6", @@ -2123,6 +2871,35 @@ "node": "*" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3879,6 +4656,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-module-shims": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.10.1.tgz", @@ -3915,6 +4699,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4305,6 +5131,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7867,6 +8703,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -9395,6 +10243,16 @@ "semver": "bin/semver.js" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9595,6 +10453,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9964,6 +10833,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", @@ -10025,6 +10901,76 @@ "node": ">= 6" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -11363,6 +12309,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -11377,6 +12330,21 @@ "dev": true, "license": "ISC" }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11499,6 +12467,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -11509,6 +12484,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12119,6 +13101,23 @@ "node": ">=0.10.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12135,6 +13134,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12211,6 +13220,16 @@ "dev": true, "license": "MIT" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -12882,6 +13901,159 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -13029,6 +14201,23 @@ "dev": true, "license": "ISC" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 1af6caea..0cb3f9ef 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "test:jsx": "c8 node --import ./test-register.js --experimental-strip-types ./node_modules/mocha/bin/mocha \"./test/**/**/*.spec.js\"", "test:tdd": "npm run test -- --watch", "test:tdd:jsx": "npm run test:jsx -- --watch", + "test:docs": "vitest run --coverage", + "test:docs:tdd": "vitest", "prepare": "husky" }, "dependencies": { @@ -73,6 +75,8 @@ "@mapbox/rehype-prism": "^0.8.0", "@types/mocha": "^10.0.10", "@types/node": "^22.13.4", + "@vitest/browser-playwright": "^4.0.18", + "@vitest/coverage-v8": "^4.0.18", "c8": "^7.11.2", "chai": "^4.3.6", "eslint": "^9.39.1", @@ -85,6 +89,7 @@ "lint-staged": "^16.2.6", "mocha": "^9.2.2", "open-props": "^1.7.4", + "playwright": "^1.58.2", "prettier": "^3.6.2", "prism-themes": "^1.9.0", "prismjs": "^1.28.0", @@ -96,6 +101,8 @@ "stylelint": "^16.10.0", "stylelint-config-recommended": "^14.0.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.2", + "vite": "^7.3.1", + "vitest": "^4.0.18" } } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..627823d0 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,120 @@ +import { defineConfig } from 'vitest/config'; +import { playwright } from '@vitest/browser-playwright'; +import { parseJsx } from './src/jsx-loader.js'; +import { generate } from 'astring'; // comes from @greenwood/plugin-import-jsx +import { greenwoodPluginImportRaw } from '@greenwood/plugin-import-raw'; +// @ts-expect-error +import { greenwoodPluginStandardCss } from '@greenwood/cli/src/plugins/resource/plugin-standard-css.js'; +// @ts-expect-error +import { readAndMergeConfig } from '@greenwood/cli/src/lifecycles/config.js'; +// @ts-expect-error +import { initContext } from '@greenwood/cli/src/lifecycles/context.js'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import type { Plugin } from 'vite'; + +// initialize Greenwood context and plugins +const config = await readAndMergeConfig(); +const context = await initContext({ config }); +const compilation = { context, config }; +// @ts-expect-error +const rawResource = greenwoodPluginImportRaw()[0].provider(compilation); +const standardCssResource = greenwoodPluginStandardCss.provider(compilation); + +function transformJsx(): Plugin { + return { + name: 'transform-jsx', + enforce: 'pre', + transform: (src, id) => { + if (id.endsWith('.tsx') || id.endsWith('.jsx')) { + const tree = parseJsx(new URL(`file://${id}`)); + const contents = generate(tree); + + return { + code: contents, + map: null, + }; + } + }, + }; +} + +function transformConstructableStylesheetsPlugin(): Plugin { + return { + name: 'transform-constructable-stylesheets', + enforce: 'pre', + resolveId: (id, importer) => { + if ( + // you'll need to configure this `importer` line to the location of your own components + importer?.indexOf('/docs/components/') >= 0 && + id.endsWith('.css') && + !id.endsWith('.module.css') + ) { + // append .type to the end of Constructable Stylesheet file paths so that they are not automatically precessed by Vite's default pipeline + return path.join(path.dirname(importer), `${id}.type`); + } + }, + load: async (id) => { + if (id.endsWith('.css.type')) { + const filename = id.slice(0, -5); + const contents = await fs.readFile(filename, 'utf-8'); + const url = new URL(`file://${id.replace('.type', '')}`); + // "coerce" native constructable stylesheets into inline JS so Vite / Rollup do not complain + const request = new Request(url, { + headers: { + Accept: 'text/javascript', + }, + }); + const response = await standardCssResource.intercept(url, request, new Response(contents)); + const body = await response.text(); + + return body; + } + }, + }; +} + +function transformRawImports(): Plugin { + return { + name: 'transform-raw-imports', + enforce: 'pre', + transform: async (src, id) => { + if (id.endsWith('?type=raw')) { + const url = new URL(`file://${id}`); + const contents = await fs.readFile(url, 'utf-8'); + const response = await rawResource.intercept(url, null, new Response(contents)); + const body = await response.text(); + + return { + code: body, + map: null, + }; + } + }, + }; +} + +export default defineConfig({ + test: { + include: ['./docs/**/*.test.ts'], + browser: { + provider: playwright(), + enabled: true, + headless: true, + instances: [{ browser: 'chromium' }], + screenshotFailures: false, + }, + coverage: { + provider: 'v8', + include: ['./docs/components/**'], + exclude: ['./docs/components/sandbox/**'], + thresholds: { + lines: 65, + functions: 75, + branches: 50, + statements: 65, + }, + }, + }, + plugins: [transformJsx(), transformRawImports(), transformConstructableStylesheetsPlugin()], +});