From 3430b33c3e6848b18749136c9ea6a5d417480125 Mon Sep 17 00:00:00 2001 From: dhellgartner <116464099+dhellgartner@users.noreply.github.com> Date: Thu, 16 Feb 2023 21:04:11 +0100 Subject: [PATCH] Alert test (#1189) * Improved tests for SlAlert * added more test for coverage * Grouped tests in multiple subgroups * remove executing only one tests * Fix the now executing tests --------- Co-authored-by: Dominikus Hellgartner --- src/components/alert/alert.test.ts | 340 +++++++++++++++++++++++------ src/internal/test.ts | 42 +++- 2 files changed, 308 insertions(+), 74 deletions(-) diff --git a/src/components/alert/alert.test.ts b/src/components/alert/alert.test.ts index bb6d4bdae..0126a923e 100644 --- a/src/components/alert/alert.test.ts +++ b/src/components/alert/alert.test.ts @@ -1,91 +1,305 @@ -import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { clickOnElement, moveMouseOnElement } from '../../internal/test'; +import { queryByTestId } from '../../internal/test/data-testid-helpers'; +import { resetMouse } from '@web/test-runner-commands'; import sinon from 'sinon'; import type SlAlert from './alert'; +import type SlIconButton from '../icon-button/icon-button'; + +const getAlertContainer = (alert: SlAlert): HTMLElement => { + return alert.shadowRoot!.querySelector('[part="base"]')!; +}; + +const expectAlertToBeVisible = (alert: SlAlert): void => { + const alertContainer = getAlertContainer(alert); + const style = window.getComputedStyle(alertContainer); + expect(style.display).not.to.equal('none'); + expect(style.visibility).not.to.equal('hidden'); + expect(style.visibility).not.to.equal('collapse'); +}; + +const expectAlertToBeInvisible = (alert: SlAlert): void => { + const alertContainer = getAlertContainer(alert); + const style = window.getComputedStyle(alertContainer); + expect(style.display, 'alert should be invisible').to.equal('none'); +}; + +const expectHideAndAfterHideToBeEmittedInCorrectOrder = async (alert: SlAlert, action: () => void | Promise) => { + const hidePromise = oneEvent(alert, 'sl-hide'); + const afterHidePromise = oneEvent(alert, 'sl-after-hide'); + let afterHideHappened = false; + oneEvent(alert, 'sl-after-hide').then(() => (afterHideHappened = true)); + + action(); + + await hidePromise; + expect(afterHideHappened).to.be.false; + + await afterHidePromise; + expectAlertToBeInvisible(alert); +}; + +const expectShowAndAfterShowToBeEmittedInCorrectOrder = async (alert: SlAlert, action: () => void | Promise) => { + const showPromise = oneEvent(alert, 'sl-show'); + const afterShowPromise = oneEvent(alert, 'sl-after-show'); + let afterShowHappened = false; + oneEvent(alert, 'sl-after-show').then(() => (afterShowHappened = true)); + + action(); + + await showPromise; + expect(afterShowHappened).to.be.false; + + await afterShowPromise; + expectAlertToBeVisible(alert); +}; + +const getCloseButton = (alert: SlAlert): SlIconButton | null | undefined => + alert.shadowRoot?.querySelector('[part="close-button"]'); describe('', () => { - it('should be visible with the open attribute', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; + let clock: sinon.SinonFakeTimers | null = null; - expect(base.hidden).to.be.false; + afterEach(async () => { + clock?.restore(); + await resetMouse(); }); - it('should not be visible without the open attribute', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; + it('renders', async () => { + const alert = await fixture(html`I am an alert`); - expect(base.hidden).to.be.true; + expectAlertToBeVisible(alert); }); - it('should emit sl-show and sl-after-show when calling show()', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; - const showHandler = sinon.spy(); - const afterShowHandler = sinon.spy(); + it('is accessible', async () => { + const alert = await fixture(html`I am an alert`); - el.addEventListener('sl-show', showHandler); - el.addEventListener('sl-after-show', afterShowHandler); - el.show(); - - await waitUntil(() => showHandler.calledOnce); - await waitUntil(() => afterShowHandler.calledOnce); - - expect(showHandler).to.have.been.calledOnce; - expect(afterShowHandler).to.have.been.calledOnce; - expect(base.hidden).to.be.false; + await expect(alert).to.be.accessible(); }); - it('should emit sl-hide and sl-after-hide when calling hide()', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; - const hideHandler = sinon.spy(); - const afterHideHandler = sinon.spy(); + describe('alert visibility', () => { + it('should be visible with the open attribute', async () => { + const alert = await fixture(html`I am an alert`); - el.addEventListener('sl-hide', hideHandler); - el.addEventListener('sl-after-hide', afterHideHandler); - el.hide(); + expectAlertToBeVisible(alert); + }); - await waitUntil(() => hideHandler.calledOnce); - await waitUntil(() => afterHideHandler.calledOnce); + it('should not be visible without the open attribute', async () => { + const alert = await fixture(html` I am an alert`); - expect(hideHandler).to.have.been.calledOnce; - expect(afterHideHandler).to.have.been.calledOnce; - expect(base.hidden).to.be.true; + expectAlertToBeInvisible(alert); + }); + + it('should emit sl-show and sl-after-show when calling show()', async () => { + const alert = await fixture(html` I am an alert`); + + expectAlertToBeInvisible(alert); + + await expectShowAndAfterShowToBeEmittedInCorrectOrder(alert, () => alert.show()); + }); + + it('should emit sl-hide and sl-after-hide when calling hide()', async () => { + const alert = await fixture(html` I am an alert`); + + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => alert.hide()); + }); + + it('should emit sl-show and sl-after-show when setting open = true', async () => { + const alert = await fixture(html` I am an alert `); + + await expectShowAndAfterShowToBeEmittedInCorrectOrder(alert, () => { + alert.open = true; + }); + }); + + it('should emit sl-hide and sl-after-hide when setting open = false', async () => { + const alert = await fixture(html` I am an alert `); + + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => { + alert.open = false; + }); + }); }); - it('should emit sl-show and sl-after-show when setting open = true', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; - const showHandler = sinon.spy(); - const afterShowHandler = sinon.spy(); + describe('close button', () => { + it('shows a close button if the alert has the closable attribute', () => async () => { + const alert = await fixture(html` I am an alert `); + const closeButton = getCloseButton(alert); - el.addEventListener('sl-show', showHandler); - el.addEventListener('sl-after-show', afterShowHandler); - el.open = true; + expect(closeButton).to.be.visible; + }); - await waitUntil(() => showHandler.calledOnce); - await waitUntil(() => afterShowHandler.calledOnce); + it('clicking the close button closes the alert', () => async () => { + const alert = await fixture(html` I am an alert `); + const closeButton = getCloseButton(alert); - expect(showHandler).to.have.been.calledOnce; - expect(afterShowHandler).to.have.been.calledOnce; - expect(base.hidden).to.be.false; + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => { + clickOnElement(closeButton!); + }); + }); }); - it('should emit sl-hide and sl-after-hide when setting open = false', async () => { - const el = await fixture(html` I am an alert `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; - const hideHandler = sinon.spy(); - const afterHideHandler = sinon.spy(); + describe('toast', () => { + const getToastStack = (): HTMLDivElement | null => document.querySelector('.sl-toast-stack'); - el.addEventListener('sl-hide', hideHandler); - el.addEventListener('sl-after-hide', afterHideHandler); - el.open = false; + const closeRemainingAlerts = async (): Promise => { + const toastStack = getToastStack(); + if (toastStack?.children) { + for (const element of toastStack.children) { + await (element as SlAlert).hide(); + } + } + }; - await waitUntil(() => hideHandler.calledOnce); - await waitUntil(() => afterHideHandler.calledOnce); + beforeEach(async () => { + await closeRemainingAlerts(); + }); - expect(hideHandler).to.have.been.calledOnce; - expect(afterHideHandler).to.have.been.calledOnce; - expect(base.hidden).to.be.true; + it('can be rendered as a toast', async () => { + const alert = await fixture(html`I am an alert`); + + expectShowAndAfterShowToBeEmittedInCorrectOrder(alert, () => alert.toast()); + const toastStack = getToastStack(); + expect(toastStack).to.be.visible; + expect(toastStack?.firstChild).to.be.equal(alert); + }); + + it('resolves only after being closed', async () => { + const alert = await fixture(html`I am an alert`); + + const afterShowEvent = oneEvent(alert, 'sl-after-show'); + let toastPromiseResolved = false; + alert.toast().then(() => (toastPromiseResolved = true)); + + await afterShowEvent; + expect(toastPromiseResolved).to.be.false; + + const closePromise = oneEvent(alert, 'sl-after-hide'); + const closeButton = getCloseButton(alert); + clickOnElement(closeButton!); + + await closePromise; + await aTimeout(0); + + expect(toastPromiseResolved).to.be.true; + }); + + const expectToastStack = () => { + const toastStack = getToastStack(); + expect(toastStack).not.to.be.null; + }; + + const expectNoToastStack = () => { + const toastStack = getToastStack(); + expect(toastStack).to.be.null; + }; + + const openToast = async (alert: SlAlert): Promise => { + const openPromise = oneEvent(alert, 'sl-after-show'); + alert.toast(); + await openPromise; + }; + + const closeToast = async (alert: SlAlert): Promise => { + const closePromise = oneEvent(alert, 'sl-after-hide'); + const closeButton = getCloseButton(alert); + await clickOnElement(closeButton!); + await closePromise; + await aTimeout(0); + }; + + it('deletes the toast stack after the last alert is done', async () => { + const container = await fixture(html`
+ alert 1 + alert 2 +
`); + + const alert1 = queryByTestId(container, 'alert1'); + const alert2 = queryByTestId(container, 'alert2'); + + await openToast(alert1!); + + expectToastStack(); + + await openToast(alert2!); + + expectToastStack(); + + await closeToast(alert1!); + + expectToastStack(); + + await closeToast(alert2!); + + expectNoToastStack(); + }); + }); + + describe('timer controlled closing', () => { + it('closes after a predefined amount of time', async () => { + clock = sinon.useFakeTimers(); + const alert = await fixture(html` I am an alert`); + + expectAlertToBeVisible(alert); + + clock.tick(2999); + + expectAlertToBeVisible(alert); + + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => { + clock?.tick(1); + }); + }); + + it('resets the closing timer after mouse-over', async () => { + clock = sinon.useFakeTimers(); + const alert = await fixture(html` I am an alert`); + + expectAlertToBeVisible(alert); + + clock.tick(1000); + + await moveMouseOnElement(alert); + + clock.tick(2999); + + expectAlertToBeVisible(alert); + + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => { + clock?.tick(1); + }); + }); + + it('resets the closing timer after opening', async () => { + clock = sinon.useFakeTimers(); + const alert = await fixture(html` I am an alert`); + + expectAlertToBeInvisible(alert); + + clock.tick(1000); + + const afterShowPromise = oneEvent(alert, 'sl-after-show'); + alert.show(); + await afterShowPromise; + + clock.tick(2999); + + await expectHideAndAfterHideToBeEmittedInCorrectOrder(alert, () => { + clock?.tick(1); + }); + }); + }); + + describe('alert variants', () => { + const variants = ['primary', 'success', 'neutral', 'warning', 'danger']; + + variants.forEach(variant => { + it(`adapts to the variant: ${variant}`, async () => { + const alert = await fixture(html`I am an alert`); + + const alertContainer = getAlertContainer(alert); + expect(alertContainer).to.have.class(`alert--${variant}`); + }); + }); }); }); diff --git a/src/internal/test.ts b/src/internal/test.ts index 935ca70c9..e4dc703c7 100644 --- a/src/internal/test.ts +++ b/src/internal/test.ts @@ -1,16 +1,6 @@ import { sendMouse } from '@web/test-runner-commands'; -/** A testing utility that measures an element's position and clicks on it. */ -export async function clickOnElement( - /** The element to click */ - el: Element, - /** The location of the element to click */ - position: 'top' | 'right' | 'bottom' | 'left' | 'center' = 'center', - /** The horizontal offset to apply to the position when clicking */ - offsetX = 0, - /** The vertical offset to apply to the position when clicking */ - offsetY = 0 -) { +function determineMousePosition(el: Element, position: string, offsetX: number, offsetY: number) { const { x, y, width, height } = el.getBoundingClientRect(); const centerX = Math.floor(x + window.pageXOffset + width / 2); const centerY = Math.floor(y + window.pageYOffset + height / 2); @@ -41,6 +31,36 @@ export async function clickOnElement( clickX += offsetX; clickY += offsetY; + return { clickX, clickY }; +} + +/** A testing utility that measures an element's position and clicks on it. */ +export async function clickOnElement( + /** The element to click */ + el: Element, + /** The location of the element to click */ + position: 'top' | 'right' | 'bottom' | 'left' | 'center' = 'center', + /** The horizontal offset to apply to the position when clicking */ + offsetX = 0, + /** The vertical offset to apply to the position when clicking */ + offsetY = 0 +) { + const { clickX, clickY } = determineMousePosition(el, position, offsetX, offsetY); await sendMouse({ type: 'click', position: [clickX, clickY] }); } + +export async function moveMouseOnElement( + /** The element to click */ + el: Element, + /** The location of the element to click */ + position: 'top' | 'right' | 'bottom' | 'left' | 'center' = 'center', + /** The horizontal offset to apply to the position when clicking */ + offsetX = 0, + /** The vertical offset to apply to the position when clicking */ + offsetY = 0 +) { + const { clickX, clickY } = determineMousePosition(el, position, offsetX, offsetY); + + await sendMouse({ type: 'move', position: [clickX, clickY] }); +}