From 7578037da5a25a6c2c94b9929ea476b54800e27c Mon Sep 17 00:00:00 2001 From: Ryan Gossiaux Date: Wed, 29 Dec 2021 10:53:12 -1000 Subject: [PATCH] Add Tabs tests --- src/lib/components/tabs/tabs.test.ts | 1836 ++++++++++++++++++++++++++ 1 file changed, 1836 insertions(+) create mode 100644 src/lib/components/tabs/tabs.test.ts diff --git a/src/lib/components/tabs/tabs.test.ts b/src/lib/components/tabs/tabs.test.ts new file mode 100644 index 0000000..82e9efe --- /dev/null +++ b/src/lib/components/tabs/tabs.test.ts @@ -0,0 +1,1836 @@ +import { render } from "@testing-library/svelte"; +import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs"; +import TestRenderer from "$lib/test-utils/TestRenderer.svelte"; +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "."; +import { assertActiveElement, assertTabs, getByText, getTabs } from "$lib/test-utils/accessibility-assertions"; +import { click, Keys, press, shift } from "$lib/test-utils/interactions"; +import Button from "$lib/internal/elements/Button.svelte"; + +let id = 0; +jest.mock('../../hooks/use-id', () => { + return { + useId: jest.fn(() => ++id), + } +}) + +beforeEach(() => id = 0) +beforeAll(() => { + // jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any) + // jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any) +}) +afterAll(() => jest.restoreAllMocks()) + +describe('safeguards', () => { + it.each([ + ['TabList', TabList], + ['Tab', Tab], + ['TabPanels', TabPanels], + ['TabPanel', TabPanel], + ])( + 'should error when we are using a <%s /> without a parent component', + suppressConsoleLogs((name, Component) => { + expect(() => render(Component)).toThrowError( + `<${name} /> is missing a parent component.` + ) + }) + ) + + it('should be possible to render TabGroup without crashing', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + ] + }) + + assertTabs({ active: 0 }) + }) +}) + +describe('Rendering', () => { + it('should be possible to render the Tab.Panels first, then the Tab.List', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + ]], + ] + }) + + assertTabs({ active: 0 }) + }) + + // describe('`renderProps`', () => { + // it('should expose the `selectedIndex` on the `Tab.Group` component', async () => { + // render( + // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ + // + // Tab 1 + // Tab 2 + // Tab 3 + // + + // + // Content 1 + // Content 2 + // Content 3 + // + // + // )} + //
+ // ) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 0 }) + // ) + + // await click(getByText('Tab 2')) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 1 }) + // ) + // }) + + // it('should expose the `selectedIndex` on the `Tab.List` component', async () => { + // render( + // + // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Tab 1 + // Tab 2 + // Tab 3 + // + // )} + //
+ + // + // Content 1 + // Content 2 + // Content 3 + // + //
+ // ) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 0 }) + // ) + + // await click(getByText('Tab 2')) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 1 }) + // ) + // }) + + // it('should expose the `selectedIndex` on the `Tab.Panels` component', async () => { + // render( + // + // + // Tab 1 + // Tab 2 + // Tab 3 + // + + // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Content 1 + // Content 2 + // Content 3 + // + // )} + //
+ //
+ // ) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 0 }) + // ) + + // await click(getByText('Tab 2')) + + // expect(document.getElementById('exposed')).toHaveTextContent( + // JSON.stringify({ selectedIndex: 1 }) + // ) + // }) + + // it('should expose the `selected` state on the `Tab` components', async () => { + // render( + // + // + // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Tab 1 + // + // )} + //
+ // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Tab 2 + // + // )} + //
+ // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Tab 3 + // + // )} + //
+ //
+ + // + // Content 1 + // Content 2 + // Content 3 + // + //
+ // ) + + // expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( + // JSON.stringify({ selected: true }) + // ) + // expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + + // await click(getTabs()[1]) + + // expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( + // JSON.stringify({ selected: true }) + // ) + // expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // }) + + // it('should expose the `selected` state on the `Tab.Panel` components', async () => { + // render( + // + // + // Tab 1 + // Tab 2 + // Tab 3 + // + + // + // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Content 1 + // + // )} + //
+ // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Content 2 + // + // )} + //
+ // + // {data => ( + // <> + //
{JSON.stringify(data)}
+ // Content 3 + // + // )} + //
+ //
+ //
+ // ) + + // expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( + // JSON.stringify({ selected: true }) + // ) + // expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + + // await click(getByText('Tab 2')) + + // expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( + // JSON.stringify({ selected: true }) + // ) + // expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( + // JSON.stringify({ selected: false }) + // ) + // }) + // }) + + describe('`defaultIndex`', () => { + it('should jump to the nearest tab when the defaultIndex is out of bounds (-2)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: -2 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + }) + + it('should jump to the nearest tab when the defaultIndex is out of bounds (+5)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 5 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 2 }) + assertActiveElement(getByText('Tab 3')) + }) + + it('should jump to the next available tab when the defaultIndex is a disabled tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 0 }, [ + [TabList, {}, [ + [Tab, { disabled: true }, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) + }) + + it('should jump to the next available tab when the defaultIndex is a disabled tab and wrap around', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, { disabled: true }, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + }) + }) + + describe(`'Tab'`, () => { + describe('`type` attribute', () => { + it('should set the `type` to "button" by default', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Trigger"], + ]], + ]], + ] + }) + + expect(getTabs()[0]).toHaveAttribute('type', 'button') + }) + + it('should not set the `type` to "button" if it already contains a `type`', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, { type: "submit" }, "Trigger"], + ]], + ]], + ] + }) + + expect(getTabs()[0]).toHaveAttribute('type', 'submit') + }) + + it('should not set the type if the "as" prop is not a "button"', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, { as: "div" }, "Trigger"], + ]], + ]], + ] + }) + + expect(getTabs()[0]).not.toHaveAttribute('type') + }) + + }) + }) +}) + +describe('Keyboard interactions', () => { + describe('`Tab` key', () => { + it('should be possible to tab to the default initial first tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + + await press(Keys.Tab) + assertActiveElement(getByText('Content 1')) + + await press(Keys.Tab) + assertActiveElement(getByText('after')) + + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Content 1')) + + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Tab 1')) + }) + + it('should be possible to tab to the default index tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) + + await press(Keys.Tab) + assertActiveElement(getByText('Content 2')) + + await press(Keys.Tab) + assertActiveElement(getByText('after')) + + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Content 2')) + + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Tab 2')) + }) + }) + + describe('`ArrowRight` key', () => { + it('should be possible to go to the next item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + }) + + it('should be possible to go to the next item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + + it('should wrap around at the end (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + }) + + it('should wrap around at the end (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + }) + + it('should not be possible to go right when in vertical mode (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowRight) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + + it('should not be possible to go right when in vertical mode (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowRight) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + }) + + describe('`ArrowLeft` key', () => { + it('should be possible to go to the previous item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) + }) + + it('should be possible to go to the previous item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + + it('should wrap around at the beginning (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + }) + + it('should wrap around at the beginning (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + }) + + it('should not be possible to go left when in vertical mode (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowLeft) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + + it('should not be possible to go left when in vertical mode (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowLeft) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + }) + + describe('`ArrowDown` key', () => { + it('should be possible to go to the next item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) + }) + + it('should be possible to go to the next item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) + }) + + it('should wrap around at the end (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + + it('should wrap around at the end (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + + it('should not be possible to go down when in horizontal mode (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowDown) + // no-op + assertTabs({ active: 0 }) + }) + + it('should not be possible to go down when in horizontal mode (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowDown) + assertTabs({ active: 0 }) + await press(Keys.Enter) + + // no-op + assertTabs({ active: 0 }) + }) + }) + + describe('`ArrowUp` key', () => { + it('should be possible to go to the previous item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) + }) + + it('should be possible to go to the previous item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) + }) + + it('should wrap around at the beginning (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, vertical: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + + it('should wrap around at the beginning (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 2, vertical: true, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) + + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + + it('should not be possible to go left when in vertical mode (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, {}, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowUp) + // no-op + assertTabs({ active: 0 }) + }) + + it('should not be possible to go left when in vertical mode (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowUp) + assertTabs({ active: 0 }) + await press(Keys.Enter) + + // no-op + assertTabs({ active: 0 }) + }) + }) + + describe('`Home` key', () => { + it('should be possible to go to the first focusable item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.Home) + assertTabs({ active: 0 }) + }) + + it('should be possible to go to the first focusable item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.Home) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + }) + + describe('`PageUp` key', () => { + it('should be possible to go to the first focusable item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.PageUp) + assertTabs({ active: 0 }) + }) + + it('should be possible to go to the first focusable item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.PageUp) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + }) + + describe('`End` key', () => { + it('should be possible to go to the first focusable item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.End) + assertTabs({ active: 2 }) + }) + + it('should be possible to go to the first focusable item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.End) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + }) + + describe('`PageDown` key', () => { + it('should be possible to go to the first focusable item (activation = `auto`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.PageDown) + assertTabs({ active: 2 }) + }) + + it('should be possible to go to the first focusable item (activation = `manual`)', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1, manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.PageDown) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + }) + + describe('`Enter` key', () => { + it('should be possible to activate the focused tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + getByText('Tab 3')?.focus() + + assertActiveElement(getByText('Tab 3')) + assertTabs({ active: 0 }) + + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + }) + + describe('`Space` key', () => { + it('should be possible to activate the focused tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { manual: true }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + + getByText('Tab 3')?.focus() + + assertActiveElement(getByText('Tab 3')) + assertTabs({ active: 0 }) + + await press(Keys.Space) + assertTabs({ active: 2 }) + }) + }) +}) + +describe('Mouse interactions', () => { + it('should be possible to click on a tab to focus it', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await click(getByText('Tab 1')) + assertTabs({ active: 0 }) + + await click(getByText('Tab 3')) + assertTabs({ active: 2 }) + + await click(getByText('Tab 2')) + assertTabs({ active: 1 }) + }) + + it('should be a no-op when clicking on a disabled tab', async () => { + render( + TestRenderer, { + allProps: [ + [TabGroup, { defaultIndex: 1 }, [ + [TabList, {}, [ + [Tab, { disabled: true }, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + assertActiveElement(document.body) + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await click(getByText('Tab 1')) + // No-op, Tab 2 is still active + assertTabs({ active: 1 }) + }) +}) + +it('should trigger the `on:change` when the tab changes', async () => { + let changes = jest.fn() + + render( + TestRenderer, { + allProps: [ + [TabGroup, { onChange: (e: CustomEvent) => changes(e.detail) }, [ + [TabList, {}, [ + [Tab, {}, "Tab 1"], + [Tab, {}, "Tab 2"], + [Tab, {}, "Tab 3"], + ]], + [TabPanels, {}, [ + [TabPanel, {}, "Content 1"], + [TabPanel, {}, "Content 2"], + [TabPanel, {}, "Content 3"], + ]], + ]], + [Button, {}, "after"], + ] + }) + + await click(getByText('Tab 2')) + await click(getByText('Tab 3')) + await click(getByText('Tab 2')) + await click(getByText('Tab 1')) + + expect(changes).toHaveBeenCalledTimes(4) + + expect(changes).toHaveBeenNthCalledWith(1, 1) + expect(changes).toHaveBeenNthCalledWith(2, 2) + expect(changes).toHaveBeenNthCalledWith(3, 1) + expect(changes).toHaveBeenNthCalledWith(4, 0) +})