atoms-element v5.0.0
git clone https://git.pyrossh.dev/atoms-element
A simple web component library for defining your custom elements. It works on both client and server.
examples/e2e.spec.js
// Real-browser test for the example app, run with: npm run test:e2e
// Requires Chromium once: npx playwright install chromium
// Named .spec.js (not .test.js) so it's never picked up by a bare `node --test`.
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { chromium } from 'playwright';
import { createElement, getElement, html } from '../index.js';
const PORT = 3987;
const BASE_URL = `http://localhost:${PORT}/`;
let serverProcess;
let browser;
let page;
const pageErrors = [];
before(async () => {
serverProcess = spawn(process.execPath, ['examples/server.js', String(PORT)], { stdio: 'ignore' });
const deadline = Date.now() + 15000;
for (;;) {
try {
if ((await fetch(BASE_URL)).ok) break;
} catch {
// server not up yet
}
if (Date.now() > deadline) throw new Error('example server did not start in time');
await new Promise((resolve) => setTimeout(resolve, 200));
}
browser = await chromium.launch();
page = await browser.newPage();
page.on('pageerror', (err) => pageErrors.push(err.message));
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
await page.waitForSelector('app-counter');
});
after(async () => {
await browser?.close();
serverProcess?.kill();
});
test('renders each counter exactly once, with no leftover duplicate SSR markup', async () => {
const counters = page.locator('app-counter');
assert.strictEqual(await counters.count(), 2);
for (let i = 0; i < 2; i++) {
assert.strictEqual(await counters.nth(i).locator('output').count(), 1, `counter ${i} should render exactly one <output>`);
}
});
test('seeds each counter from its count attribute, and app-total sums them with no manual sync', async () => {
const first = page.locator('app-counter').nth(0);
const second = page.locator('app-counter').nth(1);
assert.strictEqual((await first.locator('output').textContent()).trim(), '5', 'first app-counter has count="5"');
assert.strictEqual((await second.locator('output').textContent()).trim(), '7', 'second app-counter has count="7"');
// computed server-side by cheerio-parsing the already-rendered preceding
// markup (both app-counter tags), via Total.watch — not a separately-
// tracked total, and no shared store between the two components.
assert.strictEqual((await page.locator('app-total h1').textContent()).trim(), 'Total of 2 Counters: 12');
});
test('clicking +/- updates the counter and the derived total', async () => {
const first = page.locator('app-counter').nth(0);
const second = page.locator('app-counter').nth(1);
// Buttons carry an aria-label ("Increment"/"Decrement") that overrides their
// visible "+"/"-" glyph as the accessible name, so query by that instead.
await first.getByRole('button', { name: 'Increment' }).click();
await first.getByRole('button', { name: 'Increment' }).click();
await first.getByRole('button', { name: 'Increment' }).click();
await second.getByRole('button', { name: 'Increment' }).click();
await first.getByRole('button', { name: 'Decrement' }).click();
// first: 5 + 3 - 1 = 7, second: 7 + 1 = 8, total: sum of both, live. Reflection
// and the MutationObserver it feeds are both async, so wait for the total to
// actually settle rather than asserting immediately after the last click.
await page.waitForFunction(() => document.querySelector('app-total h1').textContent.trim() === 'Total of 2 Counters: 15');
assert.strictEqual((await first.locator('output').textContent()).trim(), '7');
assert.strictEqual((await second.locator('output').textContent()).trim(), '8');
assert.strictEqual((await page.locator('app-total h1').textContent()).trim(), 'Total of 2 Counters: 15');
});
test('external attribute writes are adopted directly, with no separate internal state to conflict with', async () => {
// Continuing from the previous test, the first counter currently shows 7.
// count now lives only in the attribute (via prop) — there's no reducer
// value to reconcile it against, so an external write just is the new
// state, and app-total (via its watch declaration) picks it up automatically.
const first = page.locator('app-counter').nth(0);
await first.evaluate((el) => el.setAttribute('count', '30'));
await page.waitForFunction(() => document.querySelector('app-counter').querySelector('output').textContent.trim() === '30');
assert.strictEqual((await first.locator('output').textContent()).trim(), '30');
await page.waitForFunction(() => document.querySelector('app-total h1').textContent.trim() === 'Total of 2 Counters: 38');
assert.strictEqual((await page.locator('app-total h1').textContent()).trim(), 'Total of 2 Counters: 38');
});
test('no console/page errors were thrown', () => {
assert.deepStrictEqual(pageErrors, []);
});
test('hydration reuses the exact SSR DOM node, so focus survives it naturally', async () => {
// Server-render the fixture the same way examples/server.js does, so the
// markup handed to the browser has real lit hydration markers rather than
// hand-typed HTML — hydrate() only works against genuine SSR structure.
const Comp = () => html`<div class="wrap"><button>-</button><button>+</button></div>`;
createElement({ url: '/focus-test-element.js' }, Comp);
const ssrMarkup = new (getElement('focus-test-element'))().render();
const result = await page.evaluate(async (markup) => {
const { createElement, html } = await import('/index.js');
const Comp = () => html`<div class="wrap"><button>-</button><button>+</button></div>`;
createElement({ url: '/focus-test-element.js' }, Comp);
const el = document.createElement('focus-test-element');
el.innerHTML = markup;
document.body.appendChild(el);
const originalPlus = el.querySelectorAll('button')[1];
originalPlus.focus();
const focusedBefore = document.activeElement === originalPlus;
// the hydration render is scheduled on a microtask by connectedCallback; wait for it
await new Promise((resolve) => queueMicrotask(resolve));
const newPlus = el.querySelectorAll('button')[1];
const focusedAfter = document.activeElement === newPlus;
const sameNode = originalPlus === newPlus;
el.remove();
return { focusedBefore, focusedAfter, sameNode };
}, ssrMarkup);
assert.strictEqual(result.focusedBefore, true, 'button should be focused before the hydration render runs');
assert.strictEqual(result.sameNode, true, 'hydration should attach to the existing SSR node, not recreate it');
assert.strictEqual(result.focusedAfter, true, 'focus should remain on the same node through hydration');
});