import '../../../dist/zn.min.js'; import { expect, fixture, html } from '@open-wc/testing'; import type ZnInput from '../input/input.component'; import type ZnSelect from '../select/select.component'; import type ZnTranslationGroup from './translation-group.component'; import type ZnTranslations from '../translations/translations.component'; describe('', () => { // The group syncs its children from its own first update, which runs before theirs, so a child's // pending value attribute must survive the language keys the group adds. it("keeps a child's rendered value when it syncs the group language", async () => { const group = await fixture(html` `); const child = group.querySelector('zn-translations')!; await child.updateComplete; expect(child.values).to.deep.equal({en: 'Chat to us'}); const input = child.shadowRoot!.querySelector('zn-input')!; expect(input.value).to.equal('Chat to us'); }); describe('actions', () => { async function actionsFixture() { const group = await fixture(html` Auto translate Cancel Save `); group.style.setProperty('--zn-spacing-small', '16px'); await group.updateComplete; return group; } it('is not rendered when nothing is slotted into it', async () => { const group = await fixture(html` `); await group.updateComplete; expect(group.shadowRoot!.querySelector('.translation-group__actions')).to.be.null; }); it('sits at the bottom of the body, not in the footer', async () => { const group = await actionsFixture(); const actions = group.shadowRoot!.querySelector('.translation-group__actions')!; expect(actions.closest('.panel__body'), 'on the white body').to.exist; expect(actions.closest('.panel__footer')).to.be.null; const body = [...group.shadowRoot!.querySelectorAll('.panel__body > *')]; expect(body.indexOf(actions), 'last thing in the body').to.equal(body.length - 1); }); it('holds align="start" children left and the rest right, in one group each', async () => { const group = await actionsFixture(); const left = group.querySelector('#left')!.getBoundingClientRect(); const cancel = group.querySelector('#cancel')!.getBoundingClientRect(); const save = group.querySelector('#save')!.getBoundingClientRect(); const row = group.shadowRoot!.querySelector('.translation-group__actions')! .getBoundingClientRect(); expect(left.left).to.be.closeTo(row.left, 1); expect(save.right).to.be.closeTo(row.right, 1); // The right-hand pair stays together rather than spreading across the free space. expect(cancel.right).to.be.closeTo(save.left - 16, 1); }); it('takes no border from the panel between the fields and the buttons', async () => { const group = await actionsFixture(); const actions = group.shadowRoot!.querySelector('.translation-group__actions')!; expect(getComputedStyle(actions).borderTopWidth).to.equal('0px'); }); }); describe('inline', () => { it('keeps the panel chrome by default', async () => { const group = await fixture(html` `); await group.updateComplete; const panel = group.shadowRoot!.querySelector('.panel')!; expect(panel.classList.contains('panel--transparent')).to.be.false; expect(panel.classList.contains('panel--flush')).to.be.false; }); it('drops the border, background and padding so the fields align with the form', async () => { const group = await fixture(html` `); await group.updateComplete; const panel = group.shadowRoot!.querySelector('.panel')!; expect(panel.classList.contains('panel--transparent')).to.be.true; expect(panel.classList.contains('panel--flush')).to.be.true; const styles = getComputedStyle(panel); expect(styles.borderTopWidth).to.equal('0px'); expect(styles.backgroundColor).to.equal('rgba(0, 0, 0, 0)'); const body = group.shadowRoot!.querySelector('.panel__body')!; expect(getComputedStyle(body).paddingLeft).to.equal('0px'); expect(getComputedStyle(group.shadowRoot!.querySelector('.panel__header')!).paddingLeft) .to.equal('0px'); }); it('leaves the same gap under the caption as between the fields', async () => { const group = await fixture(html` `); // The spacing tokens live in the theme stylesheet, which the bundle under test does not carry. group.style.setProperty('--zn-spacing-medium', '24px'); await group.updateComplete; const header = group.shadowRoot!.querySelector('.panel__header')!; const body = group.shadowRoot!.querySelector('.panel__body')!; // zn-form-group's row gap between stacked controls. The header heads the same stack, so its gap matches. expect(getComputedStyle(body).rowGap).to.equal('24px'); expect(getComputedStyle(header).paddingBottom).to.equal('24px'); }); it("does not clip the language select's focus ring once the padding is gone", async () => { const group = await fixture(html` `); await group.updateComplete; ['.panel__inner', '.panel__content', '.panel__body'].forEach(selector => { const el = group.shadowRoot!.querySelector(selector)!; expect(getComputedStyle(el).overflow, selector).to.equal('visible'); }); }); }); describe('language select', () => { function selectOf(group: ZnTranslationGroup) { return group.shadowRoot!.querySelector('zn-select')!; } it('sits in the header, opposite the caption', async () => { const group = await fixture(html` `); await group.updateComplete; const field = group.shadowRoot!.querySelector('.translation-group__language-field')!; expect(field.getAttribute('slot')).to.equal('actions'); expect(field.closest('zn-header')).to.exist; expect(group.shadowRoot!.querySelector('.panel__body zn-select')).to.be.null; }); it('leaves the caption to the label alone, with the count on the chip', async () => { const group = await fixture(html` `); await group.updateComplete; const header = group.shadowRoot!.querySelector('.panel__header')!; expect(header.getAttribute('caption')).to.equal('Content'); expect(header.querySelector('[slot="caption"]'), 'nothing else rides the caption').to.be.null; expect(group.shadowRoot!.querySelector('.translation-group__language-label')).to.be.null; }); it('drops the fill and shadow but keeps the border, hover and focus', async () => { const group = await fixture(html` `); // The input tokens live in the theme stylesheet, which the bundle under test does not carry. group.style.setProperty('--zn-input-border-width', '1px'); group.style.setProperty('--zn-input-border-color', 'rgb(9, 9, 9)'); group.style.setProperty('--zn-focus-ring-width', '3px'); group.style.setProperty('--zn-input-focus-ring-color', 'rgb(4, 5, 6)'); await group.updateComplete; const select = selectOf(group); await select.updateComplete; const combobox = select.shadowRoot!.querySelector('[part~="combobox"]')!; const styles = getComputedStyle(combobox); expect(styles.backgroundColor).to.equal('rgba(0, 0, 0, 0)'); expect(styles.boxShadow).to.equal('none'); expect(styles.borderTopWidth, 'the border stays').to.equal('1px'); expect(styles.borderTopColor, 'the border stays').to.equal('rgb(9, 9, 9)'); select.focus(); await select.updateComplete; expect(getComputedStyle(combobox).boxShadow, 'focus stays visible').to.not.equal('none'); }); it('sizes the trigger to its language and holds the listbox open wider', async () => { const group = await fixture(html` `); await group.updateComplete; const select = selectOf(group); await select.updateComplete; // The whole point: the trigger is no longer pinned to the width the options need. expect(select.getBoundingClientRect().width).to.be.lessThan(320); const listbox = select.shadowRoot!.querySelector('[part~="listbox"]')!; expect(getComputedStyle(listbox).minWidth).to.equal('320px'); }); it('names the select for a screen reader without showing the label', async () => { const group = await fixture(html` `); await group.updateComplete; const select = selectOf(group); expect(select.label).to.equal('Edit Languages'); await select.updateComplete; const label = select.shadowRoot!.querySelector('[part~="form-control-label"]')!; expect(label.getBoundingClientRect().width).to.be.lessThan(2); }); it('renders a header for the select even with no caption', async () => { const group = await fixture(html` `); await group.updateComplete; expect(group.shadowRoot!.querySelector('.panel__header')).to.exist; expect(selectOf(group)).to.exist; }); it('keeps the header to itself, with slotted actions going to the bottom', async () => { const group = await fixture(html` Auto translate `); await group.updateComplete; const actionsSlot = group.shadowRoot!.querySelector('slot[name="actions"]')!; expect(actionsSlot.closest('zn-header'), 'the header takes no actions').to.be.null; expect(actionsSlot.closest('.panel__body'), 'they go to the body').to.exist; }); function chipsOf(group: ZnTranslationGroup) { return [...selectOf(group).querySelectorAll('zn-option zn-chip')] .map(chip => chip.textContent?.trim()); } async function groupFixture(languages: Record, values: string[]) { const group = await fixture(html` ${values.map(value => html``)} `); await Promise.all([...group.querySelectorAll('zn-translations')] .map(child => child.updateComplete)); group.requestUpdate(); await group.updateComplete; return group; } it('marks a language translated only when every child has a value for it', async () => { const group = await groupFixture( {en: 'English', fr: 'French', de: 'German'}, ['{"en":"Hello","fr":"Bonjour","de":"Hallo"}', '{"en":"Hi","fr":"Salut"}']); expect(chipsOf(group)).to.deep.equal(['Translated', 'Translated', 'Partial']); }); it('summarises progress on the closed select, leaving the options their own states', async () => { const group = await groupFixture( {en: 'English', fr: 'French', de: 'German', es: 'Spanish'}, ['{"en":"Hello","fr":"Bonjour","de":"Hallo"}', '{"en":"Hi","fr":"Salut"}']); const summary = selectOf(group).querySelector('zn-chip[slot="suffix"]')!; expect(summary.textContent?.trim()).to.equal('1/3'); expect(summary.getAttribute('type')).to.equal('warning'); expect(chipsOf(group)).to.deep.equal(['Translated', 'Translated', 'Partial', 'English']); }); it('marks the summary chip done once every target language is translated', async () => { const group = await groupFixture( {en: 'English', fr: 'French'}, ['{"en":"Hello","fr":"Bonjour"}']); const summary = selectOf(group).querySelector('zn-chip[slot="suffix"]')!; expect(summary.textContent?.trim()).to.equal('1/1'); expect(summary.getAttribute('type')).to.equal('success'); }); it('falls back to English where no child has a value', async () => { const group = await groupFixture( {en: 'English', fr: 'French'}, ['{"en":"Hello"}', '{"en":"Hi"}']); expect(chipsOf(group)).to.deep.equal(['Translated', 'English']); }); it('counts the target languages, not English', async () => { const group = await groupFixture( {en: 'English', fr: 'French', de: 'German'}, ['{"en":"Hello","fr":"Bonjour"}']); const summary = selectOf(group).querySelector('zn-chip[slot="suffix"]')!; expect(summary.textContent?.trim()).to.equal('1/2'); }); it('switches every child when the select changes', async () => { const group = await groupFixture( {en: 'English', fr: 'French'}, ['{"en":"Hello","fr":"Bonjour"}', '{"en":"Hi","fr":"Salut"}']); const select = selectOf(group); select.value = 'fr'; select.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true})); await group.updateComplete; const children = [...group.querySelectorAll('zn-translations')]; await Promise.all(children.map(child => child.updateComplete)); expect(children.map(child => child.getActiveLanguage())).to.deep.equal(['fr', 'fr']); expect(children.map(child => child.shadowRoot!.querySelector('zn-input')!.value as string)) .to.deep.equal(['Bonjour', 'Salut']); }); it('emits zn-language-change without reporting a value change', async () => { const group = await groupFixture({en: 'English', fr: 'French'}, ['{"en":"Hello"}']); let language = ''; let changes = 0; group.addEventListener('zn-language-change', (e: Event) => { language = (e as CustomEvent<{language: string}>).detail.language; }); group.addEventListener('zn-change', () => changes++); const select = selectOf(group); select.value = 'fr'; select.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true})); await group.updateComplete; expect(language).to.equal('fr'); expect(changes).to.equal(0); }); }); describe('form reset', () => { it('re-reads the children once the form has restored them', async () => { const form = await fixture(html`
`); const group = form.querySelector('zn-translation-group')!; const child = group.querySelector('zn-translations')!; await child.updateComplete; group.requestUpdate(); await group.updateComplete; child.values = {en: 'Hello', fr: ''}; child.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true})); await group.updateComplete; const summary = () => group.shadowRoot!.querySelector('zn-select zn-chip[slot="suffix"]')!.textContent?.trim(); expect(summary()).to.equal('0/1'); form.reset(); await new Promise(resolve => requestAnimationFrame(() => resolve(null))); await group.updateComplete; expect(child.values).to.deep.equal({en: 'Hello', fr: 'Bonjour'}); expect(summary()).to.equal('1/1'); }); }); });