JS: pageBuilderStore
File: tests/js/unit/page-builder/pageBuilderStore.test.js
Tests: createWidget, createColumn, createRow, createSection in admin/assets/js/src/page-builder/stores/usePageBuilderStore.js
What these functions do
These four factory functions are called every time you add something in the page builder. Their return values become the objects that live in the Zustand store — and when the page is saved, they are serialised to the sanilwb_data post meta. PHP then reads that exact shape to render the frontend shortcode.
| Factory | Called when |
|---|---|
createWidget( type, index ) |
User drops a widget into a column |
createColumn() |
A new row is created (one call per column in the chosen layout) |
createRow( layoutId, label ) |
User adds a row to a section |
createSection( layoutId, sectionLabel, rowLabel ) |
User adds a new section |
Why this is a schema contract
The sanilwb_data schema is shared between JavaScript and PHP. JavaScript writes it; PHP reads it. If a key is dropped or renamed in a factory — say uid becomes widget_uid — the PHP renderer would silently receive objects without the key it expects, and widgets would fail to render on the frontend with no visible error in the builder.
These tests act as a schema guard. They fail immediately when a refactor changes the shape, before anything reaches the browser or the database.
Tests
createWidget
const widget = createWidget( 'posts', 1 );
expect( widget ).toHaveProperty( 'id' );
expect( widget ).toHaveProperty( 'uid' );
expect( widget.type ).toBe( 'posts' );
expect( widget.values ).toHaveProperty( 'sanilwb_admin_label' );
id— internal React key for the Zustand store.uid— the unique identifier PHP uses to build the[sanilwb ...]shortcode.type— determines which widget component renders (posts,gallery,text_block, etc.).values.sanilwb_admin_label— the editable label shown in the builder panel.
createColumn
const column = createColumn( 0 );
expect( column ).toHaveProperty( 'id' );
expect( Array.isArray( column.items ) ).toBe( true );
expect( column.items.length ).toBe( 0 );
expect( column.options.sanilwb_admin_label ).toBe( 'Column 1' );
A new column always starts with an empty items[] array. The items[] model replaced the old widgets[] / nestedRow split — columns can now freely mix widget items ({ kind: 'widget', ... }) and nested row items ({ kind: 'nestedRow', ... }) in any order.
createColumn takes a zero-based index and uses it to generate a positional admin label — createColumn(0) gives 'Column 1', createColumn(1) gives 'Column 2', and so on. Every column must have a non-empty options.sanilwb_admin_label — the test enforces this as a hard rule, since that label is what the Layers panel and breadcrumb trail display when no custom label has been set.
createRow
const row = createRow( 'single-column-template', 'Row 1' );
expect( row ).toHaveProperty( 'id' );
expect( row.layoutId ).toBe( 'single-column-template' );
expect( row ).toHaveProperty( 'options' );
expect( Array.isArray( row.columns ) ).toBe( true );
expect( row.columns.length ).toBe( 1 );
createRow calls getLayoutById internally to seed the row with the right number of columns. single-column-template produces exactly one column. The test verifies this wiring — if getLayoutById stops being called, row.columns would be empty and the row would render with no columns.
createRow also takes an optional third argument, columnOffset, which it forwards to each createColumn call so labels stay globally unique across the whole page rather than restarting at "Column 1" for every row:
// No offset — first two columns in the builder.
const row1 = createRow( 'six_six-column-template', 'Row 1', 0 );
// row1.columns[0].options.sanilwb_admin_label === 'Column 1'
// row1.columns[1].options.sanilwb_admin_label === 'Column 2'
// Offset of 2 — columns 3 and 4 in the builder.
const row2 = createRow( 'six_six-column-template', 'Row 2', 2 );
// row2.columns[0].options.sanilwb_admin_label === 'Column 3'
// row2.columns[1].options.sanilwb_admin_label === 'Column 4'
The caller (addRow / addRowAfter in the store) is responsible for passing the correct running total of columns already in the section so labels never collide.
createSection
const section = createSection( 'single-column-template', 'Section 1', 'Row 1' );
expect( section ).toHaveProperty( 'id' );
expect( section ).toHaveProperty( 'options' );
expect( Array.isArray( section.rows ) ).toBe( true );
expect( section.rows.length ).toBe( 1 );
A section always starts with one initial row, created by calling createRow internally. The test confirms the section is never created empty — a section with rows: [] would display nothing in the builder and could cause crashes in the DnD system.