feat(board): canonical board definition and BoardView (T2)

Encode the board as blocks of stacked-section columns (C1) and render it as
the ASCII-styled DOM grid (C2) with hover highlight and a select emit. Layout
mirrors the physical board (Client Satisfaction above Product Architecture;
Macroplan above Defect Visualisation, both left of Feature Kanban) and boxes
use true ISO 216 proportions (1:√2, each step up scaled by √2). Both views
render from the one definition; tests pin section presence, click, hover,
layout order, and a snapshot.
This commit is contained in:
Julien Calixte
2026-05-27 22:23:54 +02:00
parent d8ff2c75c9
commit ae85d212db
7 changed files with 271 additions and 2 deletions

View File

@@ -0,0 +1,32 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`BoardView > matches the rendered board snapshot 1`] = `
"<div data-v-0ced9e89="" class="board">
<section data-v-0ced9e89="" class="block" data-block-id="value-for-customer">
<h2 data-v-0ced9e89="" class="block__title">Value for Customer</h2>
<div data-v-0ced9e89="" class="block__columns">
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a4" data-section-id="client-satisfaction"><span data-v-0ced9e89="" class="section__label">Client Satisfaction</span><span data-v-0ced9e89="" class="section__size">A4</span></button><button data-v-0ced9e89="" type="button" class="section section--a4" data-section-id="product-architecture"><span data-v-0ced9e89="" class="section__label">Product Architecture</span><span data-v-0ced9e89="" class="section__size">A4</span></button></div>
</div>
</section>
<section data-v-0ced9e89="" class="block" data-block-id="right-first-time-jit">
<h2 data-v-0ced9e89="" class="block__title">Right First Time &amp; Just in Time</h2>
<div data-v-0ced9e89="" class="block__columns">
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a4" data-section-id="macroplan"><span data-v-0ced9e89="" class="section__label">Macroplan</span><span data-v-0ced9e89="" class="section__size">A4</span></button><button data-v-0ced9e89="" type="button" class="section section--a4" data-section-id="defect-visualisation"><span data-v-0ced9e89="" class="section__label">Defect Visualisation</span><span data-v-0ced9e89="" class="section__size">A4</span></button></div>
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a2" data-section-id="feature-kanban"><span data-v-0ced9e89="" class="section__label">Feature Kanban</span><span data-v-0ced9e89="" class="section__size">A2</span></button></div>
</div>
</section>
<section data-v-0ced9e89="" class="block" data-block-id="tech-enabled-network">
<h2 data-v-0ced9e89="" class="block__title">Tech-Enabled Network of Teams</h2>
<div data-v-0ced9e89="" class="block__columns">
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a3" data-section-id="tech-working-condition"><span data-v-0ced9e89="" class="section__label">Tech Working Condition</span><span data-v-0ced9e89="" class="section__size">A3</span></button></div>
</div>
</section>
<section data-v-0ced9e89="" class="block" data-block-id="learning-organisation">
<h2 data-v-0ced9e89="" class="block__title">Building a Learning Organisation</h2>
<div data-v-0ced9e89="" class="block__columns">
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a3" data-section-id="problem-solving"><span data-v-0ced9e89="" class="section__label">Problem Solving</span><span data-v-0ced9e89="" class="section__size">A3</span></button></div>
<div data-v-0ced9e89="" class="block__column"><button data-v-0ced9e89="" type="button" class="section section--a3" data-section-id="weak-point-management"><span data-v-0ced9e89="" class="section__label">Weak Point Management</span><span data-v-0ced9e89="" class="section__size">A3</span></button></div>
</div>
</section>
</div>"
`;

52
tests/board.spec.ts Normal file
View File

@@ -0,0 +1,52 @@
// @vitest-environment nuxt
import { describe, it, expect } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import BoardView from '../app/components/BoardView.vue'
import { board, sections } from '../app/board/definition'
describe('BoardView', () => {
it('renders every block and section from the one definition', async () => {
const wrapper = await mountSuspended(BoardView)
for (const block of board) {
expect(wrapper.text()).toContain(block.title)
}
for (const section of sections) {
expect(wrapper.find(`[data-section-id="${section.id}"]`).exists()).toBe(true)
expect(wrapper.text()).toContain(section.label)
}
})
it('emits the stable section id on click', async () => {
const wrapper = await mountSuspended(BoardView)
await wrapper.find('[data-section-id="macroplan"]').trigger('click')
expect(wrapper.emitted('select')?.[0]).toEqual(['macroplan'])
})
it('highlights a section on hover', async () => {
const wrapper = await mountSuspended(BoardView)
const macroplan = wrapper.find('[data-section-id="macroplan"]')
expect(macroplan.classes()).not.toContain('is-hovered')
await macroplan.trigger('mouseenter')
expect(macroplan.classes()).toContain('is-hovered')
await macroplan.trigger('mouseleave')
expect(macroplan.classes()).not.toContain('is-hovered')
})
it('stacks and orders sections as on the physical board', async () => {
const wrapper = await mountSuspended(BoardView)
const order = wrapper
.findAll('[data-section-id]')
.map((el) => el.attributes('data-section-id'))
const at = (id: string) => order.indexOf(id)
// Product Architecture below Client Satisfaction (same column).
expect(at('client-satisfaction')).toBeLessThan(at('product-architecture'))
// Macroplan above Defect Visualisation, both left of Feature Kanban.
expect(at('macroplan')).toBeLessThan(at('defect-visualisation'))
expect(at('defect-visualisation')).toBeLessThan(at('feature-kanban'))
})
it('matches the rendered board snapshot', async () => {
const wrapper = await mountSuspended(BoardView)
expect(wrapper.html()).toMatchSnapshot()
})
})