Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1dd964d
fix(create-bestax): stop the vite-ts build shadowing its config, and …
allxsmith Oct 7, 2026
bd712d5
Merge remote-tracking branch 'origin/main' into gate-fix-947-948-star…
allxsmith Oct 8, 2026
c08635b
chore: regenerate after merging main
allxsmith Oct 8, 2026
14a81f6
test(create-bestax): update the visual baselines for the new starter …
allxsmith Oct 8, 2026
3e76314
Merge remote-tracking branch 'origin/main' into gate-fix-947-948-star…
allxsmith Oct 8, 2026
6915c08
chore: regenerate after merging main
allxsmith Oct 8, 2026
67e3678
fix(create-bestax): return focus when the starter's notification clos…
allxsmith Oct 8, 2026
a96c203
fix(create-bestax): stop the no-helpers CLAUDE.md offering helper pro…
allxsmith Oct 8, 2026
f4591e0
fix(create-bestax): keep the reason for the stylesheet order in every…
allxsmith Oct 8, 2026
f200f4a
Merge remote-tracking branch 'origin/main' into gate-fix-947-948-star…
allxsmith Oct 8, 2026
d06c2a4
chore: regenerate after merging main
allxsmith Oct 8, 2026
733e505
fix(create-bestax): keep focus on the counter when Reset disables itself
allxsmith Oct 8, 2026
91dbfd5
fix(create-bestax): announce the starter's notifications through a st…
allxsmith Oct 8, 2026
fee917e
test(create-bestax): catch viewport-suffixed helper props in the no-h…
allxsmith Oct 8, 2026
d6dbe3a
Merge remote-tracking branch 'origin/main' into work-986-threads2
allxsmith Oct 8, 2026
597c392
chore: regenerate after merging main
allxsmith Oct 8, 2026
d6b446e
test(create-bestax): catch a bare boolean helper prop in the no-helpe…
allxsmith Oct 9, 2026
b29985b
fix(create-bestax): say which helper props render nothing under the n…
allxsmith Oct 9, 2026
f1ef4df
fix(create-bestax): give each starter notification a status region of…
allxsmith Oct 9, 2026
ad6f567
Merge remote-tracking branch 'origin/main' into work-986-threads3
allxsmith Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
{
"id": "icon-libraries",
"file": "references/icon-libraries.md",
"bytes": 6229
"bytes": 6691
}
],
"examples": [
Expand Down Expand Up @@ -162,7 +162,7 @@
{
"id": "modular-build",
"file": "references/modular-build.md",
"bytes": 9431
"bytes": 9583
}
],
"examples": []
Expand Down
9 changes: 9 additions & 0 deletions create-bestax/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ manifest to every required peer of the installed library, so a new or changed pe
silently dropped at ingest.
- Templates pin the library's CSS import and icon setup — a change to bulma-ui's published
exports or flavors (`bestax.css`, `versions/*.css`) may require a template update.
- The scaffolder edits the starter `App` with string patterns, so `src/__tests__/templates.test.ts`
runs every edit against the real templates. Under a `noHelpers` flavor each helper prop in
the starter becomes a named class from `NO_HELPERS_STARTER_CLASSES` in `src/constants.ts`;
a helper prop added to the starter needs a row there, and that test fails until it has one.
The icon step runs after that swap and writes into the same `App`, so what it inserts can
use no helper prop at all; the test scans the `App` both steps leave behind.
- An icon library's `packageVersion` follows the newest arm of bestax-bulma's peer range for
it, not the newest release: on a 0.x package the caret holds the minor, so a pin past the
peer range makes npm refuse the scaffold's install. The same test holds the two together.

## Testing

Expand Down
4 changes: 4 additions & 0 deletions create-bestax/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ Each scaffolded app is tested with:
8. **Tablet View** - Responsive layout at 768×1024
9. **Mobile View** - Responsive layout at 375×667
10. **Full Page** - Complete page screenshot
11. **Layout and Accessibility** - Assertions with no screenshot: no sideways scroll at phone
width, a centered title and even card heights in every flavor, icons spaced from their
text, the notification's close button, landmarks and heading order, and an underlined
link in running text

## Visual Regression Testing

Expand Down
210 changes: 210 additions & 0 deletions create-bestax/e2e/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,213 @@ test.describe('Scaffolded App - Full Page', () => {
});
});
});

/**
* Behavior the screenshots can't pin down: each of these held in one flavor
* or at one width and broke in another, so they are asserted directly and run
* in every scenario.
*/
test.describe('Scaffolded App - Layout and Accessibility', () => {
test('does not scroll sideways at phone width', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await expect(page.locator('h1').first()).toBeVisible();

const overflow = await page.evaluate(
() =>
document.documentElement.scrollWidth -
document.documentElement.clientWidth
);
expect(overflow).toBeLessThanOrEqual(0);
});

test('centers the title and evens the card heights in every flavor', async ({
page,
}) => {
await page.goto('/');

await expect(page.locator('h1').first()).toHaveCSS('text-align', 'center');

const heights = await page
.locator('.card, .bestax-card')
.evaluateAll(cards =>
cards.map(card => Math.round(card.getBoundingClientRect().height))
);
expect(heights).toHaveLength(3);
expect(new Set(heights).size).toBe(1);
});

test('spaces each card icon from its title', async ({ page }) => {
// CI names each scenario template-flavor-icon, so whether icons were
// asked for comes from there, and a scaffold that asked for them but
// rendered none fails here rather than skipping. A run with no scenario
// set falls back to what the settled page shows.
const scenario = process.env.TEST_SCENARIO;
await page.goto('/');
await expect(page.locator('h1').first()).toBeVisible();
const titles = page.locator(
':is(.card-header-title, .bestax-card-header-title):has(:is(.icon, .bestax-icon))'
);
const wantsIcons = scenario
? !scenario.endsWith('-none')
: (await titles.count()) > 0;
test.skip(!wantsIcons, 'scaffolded without icons');
await expect(titles).toHaveCount(3);

// From the icon's right edge to where the first glyph of the title text
// is drawn, wherever in the title the text node sits.
const gaps = await titles.evaluateAll(items =>
items.map(item => {
const icon = item.querySelector('.icon, .bestax-icon')!;
const walker = document.createTreeWalker(item, NodeFilter.SHOW_TEXT);
let text: Node | null = walker.nextNode();
while (text && (icon.contains(text) || !text.textContent?.trim())) {
text = walker.nextNode();
}
const range = document.createRange();
range.selectNodeContents(text!);
return (
range.getClientRects()[0].left - icon.getBoundingClientRect().right
);
})
);
for (const gap of gaps) expect(gap).toBeGreaterThan(0);
});

test('closes the success notification from its delete button', async ({
page,
}) => {
await page.goto('/');
const toggleButton = page
.locator('button')
.filter({ hasText: 'Toggle Notification' });
await toggleButton.click();

const notification = page.locator(
'[class*="notification"][class*="is-success"]'
);
await expect(notification).toBeVisible();
// From the keyboard: the close button goes away with the notification,
// so focus has to land somewhere the next Tab continues from.
await notification
.getByRole('button', { name: 'Close notification' })
.focus();
await page.keyboard.press('Enter');
await expect(notification).toHaveCount(0);
await expect(toggleButton).toBeFocused();
});

test('keeps focus on the counter when Reset disables itself', async ({
page,
}) => {
await page.goto('/');
const counterButton = page.locator('button').filter({ hasText: 'Count:' });
const resetButton = page.locator('button').filter({ hasText: 'Reset' });
await counterButton.click();
await expect(resetButton).toBeEnabled();

// From the keyboard: Reset turns disabled at zero, and a disabled button
// gives up focus, so focus has to move somewhere the next Tab continues
// from.
await resetButton.focus();
await page.keyboard.press('Enter');
await expect(counterButton).toContainText('Count: 0');
await expect(resetButton).toBeDisabled();
await expect(counterButton).toBeFocused();
});

test('announces each notification through a status region of its own', async ({
page,
}) => {
await page.goto('/');
// A live region added along with its content may not be read out, so the
// regions are on the page, empty, before either notification is.
const regions = page.getByRole('status');
await expect(regions).toHaveCount(2);
const success = regions.nth(0);
const milestone = regions.nth(1);
await expect(success).toBeEmpty();
await expect(milestone).toBeEmpty();
const before = await regions.elementHandles();

await page
.locator('button')
.filter({ hasText: 'Toggle Notification' })
.click();
await expect(success).toContainText('Success!');
await expect(milestone).toBeEmpty();

// A status region reads out all it holds whenever any of it changes, so
// the count's notification is alone in its region, and a click does not
// read the success notification out again.
const counterButton = page.locator('button').filter({ hasText: 'Count:' });
for (let i = 0; i < 12; i++) {
await counterButton.click();
}
await expect(milestone).toHaveText("You've clicked the button 12 times!");
await expect(success).not.toContainText('clicked');

// Still the nodes that were there before anything appeared in them.
const after = await regions.elementHandles();
for (const [i, region] of before.entries()) {
expect(await after[i].evaluate((el, b) => el === b, region)).toBe(true);
}
});

// Bulma spaces a notification from what follows it in the same parent, and
// each one is alone in its region, so App.css puts that space between the
// regions. No screenshot shows both notifications at once. The space is
// Bulma's block spacing, which is also what it leaves under the first box.
test('spaces two notifications showing at once', async ({ page }) => {
await page.goto('/');
await page
.locator('button')
.filter({ hasText: 'Toggle Notification' })
.click();
const counterButton = page.locator('button').filter({ hasText: 'Count:' });
for (let i = 0; i < 11; i++) {
await counterButton.click();
}

const success = page.locator(
'[class*="notification"][class*="is-success"]'
);
const milestone = page.locator('[class*="notification"][class*="is-info"]');
const top = (await success.boundingBox())!;
const bottom = (await milestone.boundingBox())!;
const blockSpacing = await page
.locator('.box, .bulma-box, .bestax-box')
.first()
.evaluate(box => parseFloat(getComputedStyle(box).marginBottom));
expect(blockSpacing).toBeGreaterThan(0);
expect(bottom.y - (top.y + top.height)).toBeCloseTo(blockSpacing);
});

test('has one main landmark, no banners, and no skipped heading levels', async ({
page,
}) => {
await page.goto('/');

await expect(page.getByRole('main')).toHaveCount(1);
// A card header outside every landmark is exposed as a page banner.
await expect(page.getByRole('banner')).toHaveCount(0);

const levels = await page
.locator('h1, h2, h3, h4, h5, h6')
.evaluateAll(headings => headings.map(h => Number(h.tagName.slice(1))));
expect(levels[0]).toBe(1);
levels.slice(1).forEach((level, i) => {
expect(level - levels[i]).toBeLessThanOrEqual(1);
});
});

test('marks the link in running text by more than its color', async ({
page,
}) => {
await page.goto('/');
await expect(page.getByRole('link', { name: 'bestax.io' })).toHaveCSS(
'text-decoration-line',
'underline'
);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading