diff --git a/biome.json b/biome.json index cf2900d97b5..47028db958b 100644 --- a/biome.json +++ b/biome.json @@ -1,6 +1,8 @@ { + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "ignore": ["**/composer.json"] + "include": ["src/*/assets/src/**", "src/*/assets/test/**", "src/*/*.json", "src/*/*/md", "*.json", "*.md"], + "ignore": ["**/composer.json", "**/vendor", "**/node_modules"] }, "linter": { "rules": { @@ -27,7 +29,7 @@ }, "javascript": { "formatter": { - "trailingComma": "es5", + "trailingCommas": "es5", "bracketSameLine": true, "quoteStyle": "single" } diff --git a/package.json b/package.json index b038092c779..0042829a503 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "scripts": { "build": "node bin/build_javascript.js && node bin/build_styles.js", "test": "bin/run-vitest-all.sh", - "lint": "yarn workspaces run biome lint src test --apply", - "format": "biome format src/*/assets/src/*.ts src/*/assets/test/*.js {,src/*/}*.{json,md} --write", - "check-lint": "yarn workspaces run biome lint src test", - "check-format": "biome format src/*/assets/src/*.ts src/*/assets/test/*.js {,src/*/}*.{json,md}" + "lint": "biome lint --write", + "format": "biome format --write", + "check-lint": "biome lint", + "check-format": "biome format" }, "devDependencies": { "@babel/core": "^7.15.8", "@babel/preset-env": "^7.15.8", "@babel/preset-react": "^7.15.8", "@babel/preset-typescript": "^7.15.8", - "@biomejs/biome": "^1.7.3", + "@biomejs/biome": "^1.8.3", "@rollup/plugin-commonjs": "^23.0.0", "@rollup/plugin-node-resolve": "^15.0.0", "@rollup/plugin-typescript": "^10.0.0", diff --git a/src/Autocomplete/assets/test/controller.test.ts b/src/Autocomplete/assets/test/controller.test.ts index eb0fdb7ba62..3f9a1d92f67 100644 --- a/src/Autocomplete/assets/test/controller.test.ts +++ b/src/Autocomplete/assets/test/controller.test.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ - import { Application } from '@hotwired/stimulus'; import { getByTestId, waitFor } from '@testing-library/dom'; import AutocompleteController, { @@ -21,7 +20,7 @@ import { vi } from 'vitest'; const shortDelay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); -const startAutocompleteTest = async (html: string): Promise<{ container: HTMLElement, tomSelect: TomSelect }> => { +const startAutocompleteTest = async (html: string): Promise<{ container: HTMLElement; tomSelect: TomSelect }> => { const container = document.createElement('div'); container.innerHTML = html; @@ -48,7 +47,7 @@ const startAutocompleteTest = async (html: string): Promise<{ container: HTMLEle } return { container, tomSelect }; -} +}; const fetchMocker = createFetchMock(vi); describe('AutocompleteController', () => { @@ -80,7 +79,7 @@ describe('AutocompleteController', () => { }); it('connect with ajax URL on a select element', async () => { - const { container, tomSelect} = await startAutocompleteTest(` + const { container, tomSelect } = await startAutocompleteTest(` { results: [ { value: 3, - text: 'salad' + text: 'salad', }, ], - }), + }) ); fetchMock.mockResponseOnce( @@ -167,14 +166,14 @@ describe('AutocompleteController', () => { results: [ { value: 1, - text: 'pizza' + text: 'pizza', }, { value: 2, - text: 'popcorn' + text: 'popcorn', }, ], - }), + }) ); const controlInput = tomSelect.control_input; @@ -233,8 +232,8 @@ describe('AutocompleteController', () => { > `); - expect(tomSelect.settings.shouldLoad('')).toBeTruthy() - }) + expect(tomSelect.settings.shouldLoad('')).toBeTruthy(); + }); it('default min-characters will always load after first load', async () => { const { container, tomSelect } = await startAutocompleteTest(` @@ -255,10 +254,10 @@ describe('AutocompleteController', () => { results: [ { value: 1, - text: 'pizza' + text: 'pizza', }, ], - }), + }) ); // wait for the initial Ajax request to finish userEvent.click(controlInput); @@ -280,14 +279,14 @@ describe('AutocompleteController', () => { results: [ { value: 1, - text: 'pizza' + text: 'pizza', }, { value: 2, - text: 'popcorn' + text: 'popcorn', }, ], - }), + }) ); controlInput.value = 'foo'; controlInput.dispatchEvent(new Event('input')); @@ -302,18 +301,18 @@ describe('AutocompleteController', () => { results: [ { value: 1, - text: 'pizza' + text: 'pizza', }, { value: 2, - text: 'popcorn' + text: 'popcorn', }, { value: 3, - text: 'apples' + text: 'apples', }, ], - }), + }) ); controlInput.value = 'fo'; controlInput.dispatchEvent(new Event('input')); @@ -350,19 +349,19 @@ describe('AutocompleteController', () => { fetchMock.mockResponseOnce( JSON.stringify({ results: [ - {value: 1, text: 'dog1'}, - {value: 2, text: 'dog2'}, - {value: 3, text: 'dog3'}, - {value: 4, text: 'dog4'}, - {value: 5, text: 'dog5'}, - {value: 6, text: 'dog6'}, - {value: 7, text: 'dog7'}, - {value: 8, text: 'dog8'}, - {value: 9, text: 'dog9'}, - {value: 10, text: 'dog10'}, + { value: 1, text: 'dog1' }, + { value: 2, text: 'dog2' }, + { value: 3, text: 'dog3' }, + { value: 4, text: 'dog4' }, + { value: 5, text: 'dog5' }, + { value: 6, text: 'dog6' }, + { value: 7, text: 'dog7' }, + { value: 8, text: 'dog8' }, + { value: 9, text: 'dog9' }, + { value: 10, text: 'dog10' }, ], - next_page: '/path/to/autocomplete?query=&page=2' - }), + next_page: '/path/to/autocomplete?query=&page=2', + }) ); const controlInput = tomSelect.control_input; @@ -380,11 +379,11 @@ describe('AutocompleteController', () => { fetchMock.mockResponseOnce( JSON.stringify({ results: [ - {value: 11, text: 'dog11'}, - {value: 12, text: 'dog12'}, + { value: 11, text: 'dog11' }, + { value: 12, text: 'dog12' }, ], next_page: null, - }), + }) ); // trigger a scroll, this will cause TomSelect to check "shouldLoadMore" @@ -514,7 +513,7 @@ describe('AutocompleteController', () => { `; - let newTomSelect: TomSelect|null = null; + let newTomSelect: TomSelect | null = null; container.addEventListener('autocomplete:connect', (event: any) => { newTomSelect = (event.detail as AutocompleteConnectOptions).tomSelect; }); @@ -570,8 +569,10 @@ describe('AutocompleteController', () => { tomSelect.addItem('3'); tomSelect.addItem('2'); const getSelectedValues = () => { - return Array.from(selectElement.selectedOptions).map((option) => option.value).sort(); - } + return Array.from(selectElement.selectedOptions) + .map((option) => option.value) + .sort(); + }; const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement; expect(getSelectedValues()).toEqual(['2', '3']); @@ -645,7 +646,7 @@ describe('AutocompleteController', () => { const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement; expect(tomSelect.control_input.placeholder).toBe('Select a dog'); - let newTomSelect: TomSelect|null = null; + let newTomSelect: TomSelect | null = null; container.addEventListener('autocomplete:connect', (event: any) => { newTomSelect = (event.detail as AutocompleteConnectOptions).tomSelect; }); @@ -685,36 +686,36 @@ describe('AutocompleteController', () => { { group_by: ['Meat'], value: 1, - text: 'Beef' + text: 'Beef', }, { group_by: ['Meat'], value: 2, - text: 'Mutton' + text: 'Mutton', }, { group_by: ['starchy'], value: 3, - text: 'Potatoes' + text: 'Potatoes', }, { group_by: ['starchy', 'Meat'], value: 4, - text: 'chili con carne' + text: 'chili con carne', }, ], optgroups: [ { value: 'Meat', - label: 'Meat' + label: 'Meat', }, { value: 'starchy', - label: 'starchy' + label: 'starchy', }, - ] + ], }, - }), + }) ); fetchMock.mockResponseOnce( @@ -724,22 +725,22 @@ describe('AutocompleteController', () => { { group_by: ['Meat'], value: 1, - text: 'Beef' + text: 'Beef', }, { group_by: ['Meat'], value: 2, - text: 'Mutton' + text: 'Mutton', }, ], optgroups: [ { value: 'Meat', - label: 'Meat' + label: 'Meat', }, - ] - } - }), + ], + }, + }) ); const controlInput = tomSelect.control_input; @@ -801,8 +802,10 @@ describe('AutocompleteController', () => { tomSelect.addItem('3'); const getSelectedValues = () => { - return Array.from(selectElement.selectedOptions).map((option) => option.value).sort(); - } + return Array.from(selectElement.selectedOptions) + .map((option) => option.value) + .sort(); + }; const selectElement = getByTestId(container, 'main-element') as HTMLSelectElement; expect(getSelectedValues()).toEqual(['2', '3']); diff --git a/src/Chartjs/assets/test/controller.test.ts b/src/Chartjs/assets/test/controller.test.ts index 350ee6db976..4378bf4d2af 100644 --- a/src/Chartjs/assets/test/controller.test.ts +++ b/src/Chartjs/assets/test/controller.test.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ - import { Application } from '@hotwired/stimulus'; import { waitFor } from '@testing-library/dom'; import ChartjsController from '../src/controller'; @@ -17,7 +16,7 @@ import ChartjsController from '../src/controller'; // chartjs:init event has already been dispatched. So, we capture it out here. let initCallCount = 0; -const startChartTest = async (canvasHtml: string): Promise<{ canvas: HTMLCanvasElement, chart: Chart }> => { +const startChartTest = async (canvasHtml: string): Promise<{ canvas: HTMLCanvasElement; chart: Chart }> => { let chart: Chart | null = null; document.body.addEventListener('chartjs:init', () => { @@ -29,7 +28,7 @@ const startChartTest = async (canvasHtml: string): Promise<{ canvas: HTMLCanvasE }); document.body.addEventListener('chartjs:connect', (event: any) => { - chart = (event.detail).chart; + chart = event.detail.chart; document.body.classList.add('connected'); }); @@ -49,7 +48,7 @@ const startChartTest = async (canvasHtml: string): Promise<{ canvas: HTMLCanvasE } return { canvas: canvasElement, chart }; -} +}; describe('ChartjsController', () => { beforeAll(() => { @@ -95,7 +94,7 @@ describe('ChartjsController', () => { `); expect(chart.options.showLines).toBe(false); - const currentViewValue = JSON.parse((canvas.dataset.chartjsViewValue as string)); + const currentViewValue = JSON.parse(canvas.dataset.chartjsViewValue as string); currentViewValue.options.showLines = true; canvas.dataset.chartjsViewValue = JSON.stringify(currentViewValue); @@ -122,7 +121,9 @@ describe('ChartjsController', () => { expect(chart.options.showLines).toBeUndefined(); // change label: January -> NewDataJanuary - const currentViewValue = JSON.parse('{"type":"line","data":{"labels":["NewDataJanuary","February","March","April","May","June","July"],"datasets":[{"label":"My First dataset","backgroundColor":"rgb(255, 99, 132)","borderColor":"rgb(255, 99, 132)","data":[0,10,5,2,20,30,45]}]},"options":[]}'); + const currentViewValue = JSON.parse( + '{"type":"line","data":{"labels":["NewDataJanuary","February","March","April","May","June","July"],"datasets":[{"label":"My First dataset","backgroundColor":"rgb(255, 99, 132)","borderColor":"rgb(255, 99, 132)","data":[0,10,5,2,20,30,45]}]},"options":[]}' + ); canvas.dataset.chartjsViewValue = JSON.stringify(currentViewValue); await waitFor(() => { @@ -160,12 +161,14 @@ describe('ChartjsController', () => { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - datasets: [{ - label: 'My First dataset', - backgroundColor: 'rgb(255, 99, 132)', - borderColor: 'rgb(255, 99, 132)', - data: [0, 10, 5, 2, 20, 30, 45], - }], + datasets: [ + { + label: 'My First dataset', + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + data: [0, 10, 5, 2, 20, 30, 45], + }, + ], }, options: { showLines: false, diff --git a/src/Cropperjs/assets/test/controller.test.ts b/src/Cropperjs/assets/test/controller.test.ts index 5199b333c22..4cc5983fea4 100644 --- a/src/Cropperjs/assets/test/controller.test.ts +++ b/src/Cropperjs/assets/test/controller.test.ts @@ -12,7 +12,7 @@ import { getByTestId, waitFor } from '@testing-library/dom'; import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; import CropperjsController from '../src/controller'; -let cropper: Cropper|null = null; +let cropper: Cropper | null = null; // Controller used to check the actual controller was properly booted class CheckController extends Controller { @@ -35,8 +35,8 @@ const dataToJsonAttribute = (data: any) => { container.dataset.foo = JSON.stringify(data); // returns the now-escaped string, ready to be used in an HTML attribute - return container.outerHTML.match(/data-foo="(.+)"/)[1] -} + return container.outerHTML.match(/data-foo="(.+)"/)[1]; +}; describe('CropperjsController', () => { let container: HTMLElement; @@ -50,7 +50,7 @@ describe('CropperjsController', () => { data-cropperjs-public-url-value="https://symfony.com/logos/symfony_black_02.png" data-cropperjs-options-value="${dataToJsonAttribute({ viewMode: 1, - dragMode: 'move' + dragMode: 'move', })}" > diff --git a/src/Icons/tests/Fixtures/Iconify/collections.json b/src/Icons/tests/Fixtures/Iconify/collections.json index f8d057d2ff7..f48b3955c4a 100644 --- a/src/Icons/tests/Fixtures/Iconify/collections.json +++ b/src/Icons/tests/Fixtures/Iconify/collections.json @@ -12,11 +12,7 @@ "spdx": "CC-BY-4.0", "url": "https://creativecommons.org/licenses/by/4.0/" }, - "samples": [ - "location-pin", - "gem", - "folder" - ], + "samples": ["location-pin", "gem", "folder"], "height": 32, "displayHeight": 16, "category": "General", @@ -35,11 +31,7 @@ "spdx": "CC-BY-4.0", "url": "https://creativecommons.org/licenses/by/4.0/" }, - "samples": [ - "message", - "clock", - "folder" - ], + "samples": ["message", "clock", "folder"], "height": 32, "displayHeight": 16, "category": "General", diff --git a/src/LazyImage/assets/test/controller.test.ts b/src/LazyImage/assets/test/controller.test.ts index eb9fecc3514..5802fe16101 100644 --- a/src/LazyImage/assets/test/controller.test.ts +++ b/src/LazyImage/assets/test/controller.test.ts @@ -57,6 +57,9 @@ describe('LazyImageController', () => { startStimulus(); await waitFor(() => expect(img).toHaveClass('connected')); expect(img).toHaveAttribute('src', 'https://symfony.com/logos/symfony_black_03.png'); - expect(img).toHaveAttribute('srcset', 'https://symfony.com/logos/symfony_black_03.png 1x, https://symfony.com/logos/symfony_black_03_2x.png 2x'); + expect(img).toHaveAttribute( + 'srcset', + 'https://symfony.com/logos/symfony_black_03.png 1x, https://symfony.com/logos/symfony_black_03_2x.png 2x' + ); }); }); diff --git a/src/LiveComponent/assets/dist/live_controller.js b/src/LiveComponent/assets/dist/live_controller.js index b8f56e12e97..05af8986dbe 100644 --- a/src/LiveComponent/assets/dist/live_controller.js +++ b/src/LiveComponent/assets/dist/live_controller.js @@ -26,7 +26,7 @@ function parseDirectives(content) { modifiers: currentModifiers, getString: () => { return content; - } + }, }); currentActionName = ''; currentArgumentValue = ''; @@ -1760,7 +1760,7 @@ class ExternalMutationTracker { this.handleAttributeMutation(mutation); handledAttributeMutations.set(element, [ ...handledAttributeMutations.get(element), - mutation.attributeName + mutation.attributeName, ]); } break; @@ -1958,7 +1958,7 @@ class Component { const promise = this.nextRequestPromise; this.pendingActions.push({ name, - args + args, }); this.debouncedStartRequest(debounce); return promise; @@ -2039,7 +2039,8 @@ class Component { input.value = ''; } const headers = backendResponse.response.headers; - if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && !headers.get('X-Live-Redirect')) { + if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && + !headers.get('X-Live-Redirect')) { const controls = { displayError: true }; this.valueStore.pushPendingPropsBackToDirty(); this.hooks.triggerHook('response:error', backendResponse, controls); @@ -2089,7 +2090,7 @@ class Component { } catch (error) { console.error(`There was a problem with the '${this.name}' component HTML returned:`, { - id: this.id + id: this.id, }); throw error; } @@ -2182,7 +2183,7 @@ class Component { }; modal.addEventListener('click', () => closeModal(modal)); modal.setAttribute('tabindex', '0'); - modal.addEventListener('keydown', e => { + modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(modal); } @@ -2291,8 +2292,7 @@ class RequestBuilder { if (hasFingerprints) { requestData.children = children; } - if (this.csrfToken && - (actions.length || totalFiles)) { + if (this.csrfToken && (actions.length || totalFiles)) { fetchOptions.headers['X-CSRF-TOKEN'] = this.csrfToken; } if (actions.length > 0) { @@ -2426,10 +2426,16 @@ class LoadingPlugin { } throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`); }); - if (isLoading && targetedActions.length > 0 && backendRequest && !backendRequest.containsOneOfActions(targetedActions)) { + if (isLoading && + targetedActions.length > 0 && + backendRequest && + !backendRequest.containsOneOfActions(targetedActions)) { return; } - if (isLoading && targetedModels.length > 0 && backendRequest && !backendRequest.areAnyModelsUpdated(targetedModels)) { + if (isLoading && + targetedModels.length > 0 && + backendRequest && + !backendRequest.areAnyModelsUpdated(targetedModels)) { return; } let loadingDirective; @@ -2472,7 +2478,7 @@ class LoadingPlugin { if (element.hasAttribute('data-loading')) { matchingElements = [element, ...matchingElements]; } - matchingElements.forEach((element => { + matchingElements.forEach((element) => { if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) { throw new Error('Invalid Element Type'); } @@ -2481,7 +2487,7 @@ class LoadingPlugin { element, directives, }); - })); + }); return loadingDirectives; } showElement(element) { @@ -2728,7 +2734,7 @@ function getModelBinding (modelDirective) { innerModelName: innerModelName || null, shouldRender, debounce, - targetEventName + targetEventName, }; } @@ -2919,7 +2925,7 @@ class LazyPlugin { getObserver() { if (!this.intersectionObserver) { this.intersectionObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { + entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.dispatchEvent(new CustomEvent('live:appear')); observer.unobserve(entry.target); diff --git a/src/LiveComponent/assets/src/Backend/Backend.ts b/src/LiveComponent/assets/src/Backend/Backend.ts index 159d4dcdd03..b7bb6445f11 100644 --- a/src/LiveComponent/assets/src/Backend/Backend.ts +++ b/src/LiveComponent/assets/src/Backend/Backend.ts @@ -3,17 +3,17 @@ import RequestBuilder from './RequestBuilder'; export interface ChildrenFingerprints { // key is the id of the child component - [key: string]: {fingerprint: string, tag: string} + [key: string]: { fingerprint: string; tag: string }; } export interface BackendInterface { makeRequest( props: any, actions: BackendAction[], - updated: {[key: string]: any}, + updated: { [key: string]: any }, children: ChildrenFingerprints, - updatedPropsFromParent: {[key: string]: any}, - files: {[key: string]: FileList}, + updatedPropsFromParent: { [key: string]: any }, + files: { [key: string]: FileList } ): BackendRequest; } @@ -32,10 +32,10 @@ export default class implements BackendInterface { makeRequest( props: any, actions: BackendAction[], - updated: {[key: string]: any}, + updated: { [key: string]: any }, children: ChildrenFingerprints, - updatedPropsFromParent: {[key: string]: any}, - files: {[key: string]: FileList}, + updatedPropsFromParent: { [key: string]: any }, + files: { [key: string]: FileList } ): BackendRequest { const { url, fetchOptions } = this.requestBuilder.buildRequest( props, diff --git a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts index b0d1ee3baf3..328f0647b91 100644 --- a/src/LiveComponent/assets/src/Backend/RequestBuilder.ts +++ b/src/LiveComponent/assets/src/Backend/RequestBuilder.ts @@ -14,10 +14,10 @@ export default class { buildRequest( props: any, actions: BackendAction[], - updated: {[key: string]: any}, + updated: { [key: string]: any }, children: ChildrenFingerprints, - updatedPropsFromParent: {[key: string]: any}, - files: {[key: string]: FileList}, + updatedPropsFromParent: { [key: string]: any }, + files: { [key: string]: FileList } ): { url: string; fetchOptions: RequestInit } { const splitUrl = this.url.split('?'); let [url] = splitUrl; @@ -30,17 +30,20 @@ export default class { 'X-Requested-With': 'XMLHttpRequest', }; - const totalFiles = Object.entries(files).reduce( - (total, current) => total + current.length, - 0 - ); + const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0); const hasFingerprints = Object.keys(children).length > 0; if ( actions.length === 0 && totalFiles === 0 && this.method === 'get' && - this.willDataFitInUrl(JSON.stringify(props), JSON.stringify(updated), params, JSON.stringify(children), JSON.stringify(updatedPropsFromParent)) + this.willDataFitInUrl( + JSON.stringify(props), + JSON.stringify(updated), + params, + JSON.stringify(children), + JSON.stringify(updatedPropsFromParent) + ) ) { params.set('props', JSON.stringify(props)); params.set('updated', JSON.stringify(updated)); @@ -61,10 +64,7 @@ export default class { requestData.children = children; } - if ( - this.csrfToken && - (actions.length || totalFiles) - ) { + if (this.csrfToken && (actions.length || totalFiles)) { fetchOptions.headers['X-CSRF-TOKEN'] = this.csrfToken; } @@ -85,9 +85,9 @@ export default class { const formData = new FormData(); formData.append('data', JSON.stringify(requestData)); - for(const [key, value] of Object.entries(files)) { + for (const [key, value] of Object.entries(files)) { const length = value.length; - for (let i = 0; i < length ; ++i) { + for (let i = 0; i < length; ++i) { formData.append(key, value[i]); } } @@ -100,11 +100,19 @@ export default class { return { url: `${url}${paramsString.length > 0 ? `?${paramsString}` : ''}`, fetchOptions, - } + }; } - private willDataFitInUrl(propsJson: string, updatedJson: string, params: URLSearchParams, childrenJson: string, propsFromParentJson: string) { - const urlEncodedJsonData = new URLSearchParams(propsJson + updatedJson + childrenJson + propsFromParentJson).toString(); + private willDataFitInUrl( + propsJson: string, + updatedJson: string, + params: URLSearchParams, + childrenJson: string, + propsFromParentJson: string + ) { + const urlEncodedJsonData = new URLSearchParams( + propsJson + updatedJson + childrenJson + propsFromParentJson + ).toString(); // if the URL gets remotely close to 2000 chars, it may not fit return (urlEncodedJsonData + params.toString()).length < 1500; diff --git a/src/LiveComponent/assets/src/Component/ElementDriver.ts b/src/LiveComponent/assets/src/Component/ElementDriver.ts index d1e2a720b32..7eb67bc56b1 100644 --- a/src/LiveComponent/assets/src/Component/ElementDriver.ts +++ b/src/LiveComponent/assets/src/Component/ElementDriver.ts @@ -1,20 +1,20 @@ -import {getModelDirectiveFromElement} from '../dom_utils'; +import { getModelDirectiveFromElement } from '../dom_utils'; import type LiveControllerDefault from '../live_controller'; export interface ElementDriver { - getModelName(element: HTMLElement): string|null; + getModelName(element: HTMLElement): string | null; getComponentProps(): any; /** * Given an element from a response, find all the events that should be emitted. */ - getEventsToEmit(): Array<{event: string, data: any, target: string|null, componentName: string|null }>; + getEventsToEmit(): Array<{ event: string; data: any; target: string | null; componentName: string | null }>; /** * Given an element from a response, find all the events that should be dispatched. */ - getBrowserEventsToDispatch(): Array<{event: string, payload: any }>; + getBrowserEventsToDispatch(): Array<{ event: string; payload: any }>; } export class StimulusElementDriver implements ElementDriver { @@ -24,7 +24,7 @@ export class StimulusElementDriver implements ElementDriver { this.controller = controller; } - getModelName(element: HTMLElement): string|null { + getModelName(element: HTMLElement): string | null { const modelDirective = getModelDirectiveFromElement(element, false); if (!modelDirective) { @@ -38,11 +38,11 @@ export class StimulusElementDriver implements ElementDriver { return this.controller.propsValue; } - getEventsToEmit(): Array<{event: string, data: any, target: string|null, componentName: string|null }> { + getEventsToEmit(): Array<{ event: string; data: any; target: string | null; componentName: string | null }> { return this.controller.eventsToEmitValue; } - getBrowserEventsToDispatch(): Array<{event: string, payload: any }> { + getBrowserEventsToDispatch(): Array<{ event: string; payload: any }> { return this.controller.eventsToDispatchValue; } } diff --git a/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts b/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts index c8c85f6179b..151b785a10f 100644 --- a/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts +++ b/src/LiveComponent/assets/src/Component/UnsyncedInputsTracker.ts @@ -1,5 +1,5 @@ -import type {ElementDriver} from './ElementDriver'; -import {elementBelongsToThisComponent} from '../dom_utils'; +import type { ElementDriver } from './ElementDriver'; +import { elementBelongsToThisComponent } from '../dom_utils'; import type Component from './index'; export default class { @@ -8,7 +8,7 @@ export default class { /** Fields that have changed, but whose value is not set back onto the value store */ private readonly unsyncedInputs: UnsyncedInputContainer; - private elementEventListeners: Array<{ event: string, callback: (event: any) => void }> = [ + private elementEventListeners: Array<{ event: string; callback: (event: any) => void }> = [ { event: 'input', callback: (event) => this.handleInputEvent(event) }, ]; @@ -19,13 +19,13 @@ export default class { } activate(): void { - this.elementEventListeners.forEach(({event, callback}) => { + this.elementEventListeners.forEach(({ event, callback }) => { this.component.element.addEventListener(event, callback); }); } deactivate(): void { - this.elementEventListeners.forEach(({event, callback}) => { + this.elementEventListeners.forEach(({ event, callback }) => { this.component.element.removeEventListener(event, callback); }); } @@ -40,7 +40,7 @@ export default class { return; } - this.updateModelFromElement(target) + this.updateModelFromElement(target); } private updateModelFromElement(element: Element) { @@ -97,7 +97,7 @@ export class UnsyncedInputContainer { this.unsyncedModelFields = new Map(); } - add(element: HTMLElement, modelName: string|null = null) { + add(element: HTMLElement, modelName: string | null = null) { if (modelName) { this.unsyncedModelFields.set(modelName, element); if (!this.unsyncedModelNames.includes(modelName)) { @@ -124,7 +124,7 @@ export class UnsyncedInputContainer { } allUnsyncedInputs(): HTMLElement[] { - return [...this.unsyncedNonModelFields, ...this.unsyncedModelFields.values()] + return [...this.unsyncedNonModelFields, ...this.unsyncedModelFields.values()]; } markModelAsSynced(modelName: string): void { diff --git a/src/LiveComponent/assets/src/Component/ValueStore.ts b/src/LiveComponent/assets/src/Component/ValueStore.ts index 3f5afdd8ae6..a92ded2edb5 100644 --- a/src/LiveComponent/assets/src/Component/ValueStore.ts +++ b/src/LiveComponent/assets/src/Component/ValueStore.ts @@ -12,20 +12,20 @@ export default class { /** * A list of props that have been "dirty" (changed) since the last request to the server. */ - private dirtyProps: {[key: string]: any} = {}; + private dirtyProps: { [key: string]: any } = {}; /** * A list of dirty props that were sent to the server, but the response has * not yet been received. */ - private pendingProps: {[key: string]: any} = {}; + private pendingProps: { [key: string]: any } = {}; /** * A list of props that the parent wants us to update. * * These will be sent on the next request to the server. */ - private updatedPropsFromParent: {[key: string]: any} = {}; + private updatedPropsFromParent: { [key: string]: any } = {}; constructor(props: any) { this.props = props; diff --git a/src/LiveComponent/assets/src/Component/index.ts b/src/LiveComponent/assets/src/Component/index.ts index af6ee4a952f..28807c57884 100644 --- a/src/LiveComponent/assets/src/Component/index.ts +++ b/src/LiveComponent/assets/src/Component/index.ts @@ -17,14 +17,14 @@ declare const Turbo: any; type MaybePromise = T | Promise; export type ComponentHooks = { - connect: (component: Component) => MaybePromise, - disconnect: (component: Component) => MaybePromise, - 'request:started': (requestConfig: any) => MaybePromise, - 'render:finished': (component: Component) => MaybePromise, - 'response:error': (backendResponse: BackendResponse, controls: { displayError: boolean }) => MaybePromise, - 'loading.state:started': (element: HTMLElement, request: BackendRequest) => MaybePromise, - 'loading.state:finished': (element: HTMLElement) => MaybePromise, - 'model:set': (model: string, value: any, component: Component) => MaybePromise, + connect: (component: Component) => MaybePromise; + disconnect: (component: Component) => MaybePromise; + 'request:started': (requestConfig: any) => MaybePromise; + 'render:finished': (component: Component) => MaybePromise; + 'response:error': (backendResponse: BackendResponse, controls: { displayError: boolean }) => MaybePromise; + 'loading.state:started': (element: HTMLElement, request: BackendRequest) => MaybePromise; + 'loading.state:finished': (element: HTMLElement) => MaybePromise; + 'model:set': (model: string, value: any, component: Component) => MaybePromise; }; export type ComponentHookName = keyof ComponentHooks; @@ -40,7 +40,7 @@ export default class Component { readonly listeners: Map; private backend: BackendInterface; readonly elementDriver: ElementDriver; - id: string|null; + id: string | null; /** * A fingerprint that identifies the props/input that was used on @@ -57,11 +57,11 @@ export default class Component { defaultDebounce = 150; - private backendRequest: BackendRequest|null = null; + private backendRequest: BackendRequest | null = null; /** Actions that are waiting to be executed */ private pendingActions: BackendAction[] = []; /** Files that are waiting to be sent */ - private pendingFiles: {[key: string]: HTMLInputElement} = {}; + private pendingFiles: { [key: string]: HTMLInputElement } = {}; /** Is a request waiting to be made? */ private isRequestPending = false; /** Current "timeout" before the pending request should be sent. */ @@ -80,7 +80,15 @@ export default class Component { * @param backend Backend instance for updating * @param elementDriver Class to get "model" name from any element. */ - constructor(element: HTMLElement, name: string, props: any, listeners: Array<{ event: string; action: string }>, id: string|null, backend: BackendInterface, elementDriver: ElementDriver) { + constructor( + element: HTMLElement, + name: string, + props: any, + listeners: Array<{ event: string; action: string }>, + id: string | null, + backend: BackendInterface, + elementDriver: ElementDriver + ) { this.element = element; this.name = name; this.backend = backend; @@ -100,9 +108,8 @@ export default class Component { this.hooks = new HookManager(); this.resetPromise(); - this.externalMutationTracker = new ExternalMutationTracker( - this.element, - (element: Element) => elementBelongsToThisComponent(element, this) + this.externalMutationTracker = new ExternalMutationTracker(this.element, (element: Element) => + elementBelongsToThisComponent(element, this) ); // start early to catch any mutations that happen before the component is connected // for example, the LoadingPlugin, which sets initial non-loading state @@ -128,15 +135,21 @@ export default class Component { this.externalMutationTracker.stop(); } - on(hookName: T, callback: ComponentHookCallback): void { + on( + hookName: T, + callback: ComponentHookCallback + ): void { this.hooks.register(hookName, callback); } - off(hookName: T, callback: ComponentHookCallback): void { + off( + hookName: T, + callback: ComponentHookCallback + ): void { this.hooks.unregister(hookName, callback); } - set(model: string, value: any, reRender = false, debounce: number|boolean = false): Promise { + set(model: string, value: any, reRender = false, debounce: number | boolean = false): Promise { const promise = this.nextRequestPromise; const modelName = normalizeModelName(model); @@ -167,11 +180,11 @@ export default class Component { return this.valueStore.get(modelName); } - action(name: string, args: any = {}, debounce: number|boolean = false): Promise { + action(name: string, args: any = {}, debounce: number | boolean = false): Promise { const promise = this.nextRequestPromise; this.pendingActions.push({ name, - args + args, }); this.debouncedStartRequest(debounce); @@ -198,11 +211,11 @@ export default class Component { return this.unsyncedInputsTracker.getUnsyncedModels(); } - emit(name: string, data: any, onlyMatchingComponentsNamed: string|null = null): void { + emit(name: string, data: any, onlyMatchingComponentsNamed: string | null = null): void { this.performEmit(name, data, false, onlyMatchingComponentsNamed); } - emitUp(name: string, data: any, onlyMatchingComponentsNamed: string|null = null): void { + emitUp(name: string, data: any, onlyMatchingComponentsNamed: string | null = null): void { this.performEmit(name, data, true, onlyMatchingComponentsNamed); } @@ -210,7 +223,7 @@ export default class Component { this.doEmit(name, data); } - private performEmit(name: string, data: any, emitUp: boolean, matchingName: string|null): void { + private performEmit(name: string, data: any, emitUp: boolean, matchingName: string | null): void { const components: Component[] = findComponents(this, emitUp, matchingName); components.forEach((component) => { component.doEmit(name, data); @@ -219,7 +232,7 @@ export default class Component { private doEmit(name: string, data: any): void { if (!this.listeners.has(name)) { - return ; + return; } // set actions but tell TypeScript it is an array of strings @@ -236,7 +249,7 @@ export default class Component { private tryStartingRequest(): void { if (!this.backendRequest) { - this.performRequest() + this.performRequest(); return; } @@ -255,8 +268,8 @@ export default class Component { // they are now "in sync" (with some exceptions noted inside) this.unsyncedInputsTracker.resetUnsyncedFields(); - const filesToSend: {[key: string]: FileList} = {}; - for(const [key, value] of Object.entries(this.pendingFiles)) { + const filesToSend: { [key: string]: FileList } = {}; + for (const [key, value] of Object.entries(this.pendingFiles)) { if (value.files) { filesToSend[key] = value.files; } @@ -290,13 +303,16 @@ export default class Component { const html = await backendResponse.getBody(); // clear sent files inputs - for(const input of Object.values(this.pendingFiles)) { + for (const input of Object.values(this.pendingFiles)) { input.value = ''; } // if the response does not contain a component, render as an error const headers = backendResponse.response.headers; - if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && !headers.get('X-Live-Redirect')) { + if ( + !headers.get('Content-Type')?.includes('application/vnd.live-component+html') && + !headers.get('X-Live-Redirect') + ) { const controls = { displayError: true }; this.valueStore.pushPendingPropsBackToDirty(); this.hooks.triggerHook('response:error', backendResponse, controls); @@ -370,7 +386,7 @@ export default class Component { } } catch (error) { console.error(`There was a problem with the '${this.name}' component HTML returned:`, { - id: this.id + id: this.id, }); throw error; } @@ -408,23 +424,25 @@ export default class Component { if (target === 'self') { this.emitSelf(event, data); - return + return; } this.emit(event, data, componentName); }); browserEventsToDispatch.forEach(({ event, payload }) => { - this.element.dispatchEvent(new CustomEvent(event, { - detail: payload, - bubbles: true, - })); + this.element.dispatchEvent( + new CustomEvent(event, { + detail: payload, + bubbles: true, + }) + ); }); this.hooks.triggerHook('render:finished', this); } - private calculateDebounce(debounce: number|boolean): number { + private calculateDebounce(debounce: number | boolean): number { if (debounce === true) { return this.defaultDebounce; } @@ -443,7 +461,7 @@ export default class Component { } } - private debouncedStartRequest(debounce: number|boolean) { + private debouncedStartRequest(debounce: number | boolean) { this.clearRequestDebounceTimeout(); this.requestDebounceTimeout = window.setTimeout(() => { this.render(); @@ -483,19 +501,19 @@ export default class Component { iframe.contentWindow.document.close(); } - const closeModal = (modal: HTMLElement|null) => { + const closeModal = (modal: HTMLElement | null) => { if (modal) { - modal.outerHTML = '' + modal.outerHTML = ''; } - document.body.style.overflow = 'visible' - } + document.body.style.overflow = 'visible'; + }; // close on click modal.addEventListener('click', () => closeModal(modal)); // close on escape modal.setAttribute('tabindex', '0'); - modal.addEventListener('keydown', e => { + modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(modal); } @@ -534,14 +552,14 @@ export default class Component { */ export function proxifyComponent(component: Component): Component { return new Proxy(component, { - get(component: Component, prop: string|symbol): any { + get(component: Component, prop: string | symbol): any { // string check is to handle symbols if (prop in component || typeof prop !== 'string') { if (typeof component[prop as keyof typeof component] === 'function') { const callable = component[prop as keyof typeof component] as (...args: any) => any; return (...args: any) => { return callable.apply(component, args); - } + }; } // forward to public properties @@ -550,18 +568,17 @@ export function proxifyComponent(component: Component): Component { // return model if (component.valueStore.has(prop)) { - return component.getData(prop) + return component.getData(prop); } // try to call an action return (args: string[]) => { return component.action.apply(component, [prop, args]); - } + }; }, set(target: Component, property: string, value: any): boolean { if (property in target) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Ignoring potentially setting private properties target[property as keyof typeof target] = value; diff --git a/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts index 18a1196a07a..be7b8ef3a9b 100644 --- a/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/ChildComponentPlugin.ts @@ -19,7 +19,7 @@ export default class implements PluginInterface { private parentModelBindings: ModelBinding[] = []; constructor(component: Component) { - this.component = component; + this.component = component; const modelDirectives = getAllModelDirectiveFromElements(this.component.element); this.parentModelBindings = modelDirectives.map(getModelBinding); @@ -73,12 +73,7 @@ export default class implements PluginInterface { return; } - parentComponent.set( - modelBinding.modelName, - value, - modelBinding.shouldRender, - modelBinding.debounce - ); + parentComponent.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce); }); } diff --git a/src/LiveComponent/assets/src/Component/plugins/LazyPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/LazyPlugin.ts index 2538b527a33..3cdae8ecfb2 100644 --- a/src/LiveComponent/assets/src/Component/plugins/LazyPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/LazyPlugin.ts @@ -1,8 +1,8 @@ -import type {PluginInterface} from './PluginInterface'; +import type { PluginInterface } from './PluginInterface'; import type Component from '../index'; export default class implements PluginInterface { - private intersectionObserver: IntersectionObserver | null = null; + private intersectionObserver: IntersectionObserver | null = null; attachToComponent(component: Component): void { if ('lazy' !== component.element.attributes.getNamedItem('loading')?.value) { @@ -19,7 +19,7 @@ export default class implements PluginInterface { private getObserver(): IntersectionObserver { if (!this.intersectionObserver) { this.intersectionObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { + entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.dispatchEvent(new CustomEvent('live:appear')); observer.unobserve(entry.target); diff --git a/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts index 7fa9db679d4..211a7f35155 100644 --- a/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/LoadingPlugin.ts @@ -1,8 +1,4 @@ -import { - type Directive, - type DirectiveModifier, - parseDirectives -} from '../../Directive/directives_parser'; +import { type Directive, type DirectiveModifier, parseDirectives } from '../../Directive/directives_parser'; import { elementBelongsToThisComponent } from '../../dom_utils'; import { combineSpacedArray } from '../../string_utils'; import type BackendRequest from '../../Backend/BackendRequest'; @@ -10,8 +6,8 @@ import type Component from '../../Component'; import type { PluginInterface } from './PluginInterface'; interface ElementLoadingDirectives { - element: HTMLElement|SVGElement, - directives: Directive[] + element: HTMLElement | SVGElement; + directives: Directive[]; } export default class implements PluginInterface { @@ -27,15 +23,20 @@ export default class implements PluginInterface { this.finishLoading(component, component.element); } - startLoading(component: Component, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest): void { + startLoading(component: Component, targetElement: HTMLElement | SVGElement, backendRequest: BackendRequest): void { this.handleLoadingToggle(component, true, targetElement, backendRequest); } - finishLoading(component: Component, targetElement: HTMLElement|SVGElement): void { - this.handleLoadingToggle(component,false, targetElement, null); + finishLoading(component: Component, targetElement: HTMLElement | SVGElement): void { + this.handleLoadingToggle(component, false, targetElement, null); } - private handleLoadingToggle(component: Component, isLoading: boolean, targetElement: HTMLElement|SVGElement, backendRequest: BackendRequest|null) { + private handleLoadingToggle( + component: Component, + isLoading: boolean, + targetElement: HTMLElement | SVGElement, + backendRequest: BackendRequest | null + ) { if (isLoading) { this.addAttributes(targetElement, ['busy']); } else { @@ -51,12 +52,17 @@ export default class implements PluginInterface { } directives.forEach((directive) => { - this.handleLoadingDirective(element, isLoading, directive, backendRequest) + this.handleLoadingDirective(element, isLoading, directive, backendRequest); }); }); } - private handleLoadingDirective(element: HTMLElement|SVGElement, isLoading: boolean, directive: Directive, backendRequest: BackendRequest|null) { + private handleLoadingDirective( + element: HTMLElement | SVGElement, + isLoading: boolean, + directive: Directive, + backendRequest: BackendRequest | null + ) { const finalAction = parseLoadingAction(directive.action, isLoading); const targetedActions: string[] = []; @@ -72,13 +78,17 @@ export default class implements PluginInterface { }); validModifiers.set('action', (modifier: DirectiveModifier) => { if (!modifier.value) { - throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"`); + throw new Error( + `The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"` + ); } targetedActions.push(modifier.value); }); validModifiers.set('model', (modifier: DirectiveModifier) => { if (!modifier.value) { - throw new Error(`The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"`); + throw new Error( + `The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"` + ); } targetedModels.push(modifier.value); }); @@ -92,20 +102,32 @@ export default class implements PluginInterface { return; } - throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`) + throw new Error( + `Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.` + ); }); // if loading is being activated + action modifier, only apply if the action is on the request - if (isLoading && targetedActions.length > 0 && backendRequest && !backendRequest.containsOneOfActions(targetedActions)) { + if ( + isLoading && + targetedActions.length > 0 && + backendRequest && + !backendRequest.containsOneOfActions(targetedActions) + ) { return; } // if loading is being activated + model modifier, only apply if the model is modified - if (isLoading && targetedModels.length > 0 && backendRequest && !backendRequest.areAnyModelsUpdated(targetedModels)) { + if ( + isLoading && + targetedModels.length > 0 && + backendRequest && + !backendRequest.areAnyModelsUpdated(targetedModels) + ) { return; } - let loadingDirective: (() => void); + let loadingDirective: () => void; switch (finalAction) { case 'show': @@ -149,7 +171,7 @@ export default class implements PluginInterface { loadingDirective(); } - getLoadingDirectives(component: Component, element: HTMLElement|SVGElement) { + getLoadingDirectives(component: Component, element: HTMLElement | SVGElement) { const loadingDirectives: ElementLoadingDirectives[] = []; let matchingElements = [...Array.from(element.querySelectorAll('[data-loading]'))]; @@ -161,7 +183,7 @@ export default class implements PluginInterface { matchingElements = [element, ...matchingElements]; } - matchingElements.forEach((element => { + matchingElements.forEach((element) => { if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) { throw new Error('Invalid Element Type'); } @@ -173,24 +195,24 @@ export default class implements PluginInterface { element, directives, }); - })); + }); return loadingDirectives; } - private showElement(element: HTMLElement|SVGElement) { + private showElement(element: HTMLElement | SVGElement) { element.style.display = 'revert'; } - private hideElement(element: HTMLElement|SVGElement) { + private hideElement(element: HTMLElement | SVGElement) { element.style.display = 'none'; } - private addClass(element: HTMLElement|SVGElement, classes: string[]) { + private addClass(element: HTMLElement | SVGElement, classes: string[]) { element.classList.add(...combineSpacedArray(classes)); } - private removeClass(element: HTMLElement|SVGElement, classes: string[]) { + private removeClass(element: HTMLElement | SVGElement, classes: string[]) { element.classList.remove(...combineSpacedArray(classes)); if (element.classList.length === 0) { // remove empty class="" to avoid morphdom "diff" problem @@ -201,13 +223,13 @@ export default class implements PluginInterface { private addAttributes(element: Element, attributes: string[]) { attributes.forEach((attribute) => { element.setAttribute(attribute, ''); - }) + }); } private removeAttributes(element: Element, attributes: string[]) { attributes.forEach((attribute) => { element.removeAttribute(attribute); - }) + }); } } @@ -228,5 +250,4 @@ const parseLoadingAction = (action: string, isLoading: boolean) => { } throw new Error(`Unknown data-loading action "${action}"`); -} - +}; diff --git a/src/LiveComponent/assets/src/Component/plugins/PollingPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/PollingPlugin.ts index 2272f6437b0..505ecc1d28e 100644 --- a/src/LiveComponent/assets/src/Component/plugins/PollingPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/PollingPlugin.ts @@ -52,7 +52,7 @@ export default class implements PluginInterface { duration = Number.parseInt(modifier.value); } - break; + break; default: console.warn(`Unknown modifier "${modifier.name}" in data-poll "${rawPollConfig}".`); } @@ -62,4 +62,3 @@ export default class implements PluginInterface { }); } } - diff --git a/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts index a0c0f3576a4..2d2b29ea55c 100644 --- a/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/QueryStringPlugin.ts @@ -6,11 +6,11 @@ interface QueryMapping { /** * URL parameter name */ - name: string, + name: string; } export default class implements PluginInterface { - constructor(private readonly mapping: {[p: string]: QueryMapping}) {} + constructor(private readonly mapping: { [p: string]: QueryMapping }) {} attachToComponent(component: Component): void { component.on('render:finished', (component: Component) => { diff --git a/src/LiveComponent/assets/src/Component/plugins/SetValueOntoModelFieldsPlugin.ts b/src/LiveComponent/assets/src/Component/plugins/SetValueOntoModelFieldsPlugin.ts index 944c8084dae..9606c487348 100644 --- a/src/LiveComponent/assets/src/Component/plugins/SetValueOntoModelFieldsPlugin.ts +++ b/src/LiveComponent/assets/src/Component/plugins/SetValueOntoModelFieldsPlugin.ts @@ -3,7 +3,7 @@ import { elementBelongsToThisComponent, getModelDirectiveFromElement, getValueFromElement, - setValueOnElement + setValueOnElement, } from '../../dom_utils'; import type { PluginInterface } from './PluginInterface'; @@ -56,7 +56,7 @@ export default class implements PluginInterface { } if (component.valueStore.has(modelName)) { - setValueOnElement(element, component.valueStore.get(modelName)) + setValueOnElement(element, component.valueStore.get(modelName)); } // for select elements without a blank value, one might be selected automatically @@ -64,6 +64,6 @@ export default class implements PluginInterface { if (element instanceof HTMLSelectElement && !element.multiple) { component.valueStore.set(modelName, getValueFromElement(element, component.valueStore)); } - }) + }); } } diff --git a/src/LiveComponent/assets/src/Directive/directives_parser.ts b/src/LiveComponent/assets/src/Directive/directives_parser.ts index bb5e1c1c85f..986dd1cd88d 100644 --- a/src/LiveComponent/assets/src/Directive/directives_parser.ts +++ b/src/LiveComponent/assets/src/Directive/directives_parser.ts @@ -43,7 +43,7 @@ export interface Directive { * * @param {string} content The value of the attribute */ -export function parseDirectives(content: string|null): Directive[] { +export function parseDirectives(content: string | null): Directive[] { const directives: Directive[] = []; if (!content) { @@ -53,7 +53,7 @@ export function parseDirectives(content: string|null): Directive[] { let currentActionName = ''; let currentArgumentValue = ''; let currentArguments: string[] = []; - let currentModifiers: { name: string, value: string | null }[] = []; + let currentModifiers: { name: string; value: string | null }[] = []; let state = 'action'; const getLastActionName = () => { @@ -66,7 +66,7 @@ export function parseDirectives(content: string|null): Directive[] { } return directives[directives.length - 1].action; - } + }; const pushInstruction = () => { directives.push({ action: currentActionName, @@ -76,24 +76,24 @@ export function parseDirectives(content: string|null): Directive[] { // TODO - make a string representation of JUST this directive return content; - } + }, }); currentActionName = ''; currentArgumentValue = ''; currentArguments = []; currentModifiers = []; state = 'action'; - } + }; const pushArgument = () => { // value is trimmed to avoid space after "," // "foo, bar" currentArguments.push(currentArgumentValue.trim()); currentArgumentValue = ''; - } + }; const pushModifier = () => { if (currentArguments.length > 1) { - throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`) + throw new Error(`The modifier "${currentActionName}()" does not support multiple arguments.`); } currentModifiers.push({ @@ -103,11 +103,11 @@ export function parseDirectives(content: string|null): Directive[] { currentActionName = ''; currentArguments = []; state = 'action'; - } + }; for (let i = 0; i < content.length; i++) { const char = content[i]; - switch(state) { + switch (state) { case 'action': if (char === '(') { state = 'arguments'; @@ -171,7 +171,7 @@ export function parseDirectives(content: string|null): Directive[] { // we just finished an action(), and now we need a space if (char !== ' ') { - throw new Error(`Missing space after ${getLastActionName()}()`) + throw new Error(`Missing space after ${getLastActionName()}()`); } pushInstruction(); @@ -189,7 +189,7 @@ export function parseDirectives(content: string|null): Directive[] { break; default: - throw new Error(`Did you forget to add a closing ")" after "${currentActionName}"?`) + throw new Error(`Did you forget to add a closing ")" after "${currentActionName}"?`); } return directives; diff --git a/src/LiveComponent/assets/src/Directive/get_model_binding.ts b/src/LiveComponent/assets/src/Directive/get_model_binding.ts index 033fa7d39de..7b61a6a7fd1 100644 --- a/src/LiveComponent/assets/src/Directive/get_model_binding.ts +++ b/src/LiveComponent/assets/src/Directive/get_model_binding.ts @@ -1,26 +1,30 @@ -import type {Directive} from './directives_parser'; +import type { Directive } from './directives_parser'; export interface ModelBinding { - modelName: string, - innerModelName: string|null, - shouldRender: boolean, - debounce: number|boolean, - targetEventName: string|null + modelName: string; + innerModelName: string | null; + shouldRender: boolean; + debounce: number | boolean; + targetEventName: string | null; } -export default function(modelDirective: Directive): ModelBinding { +export default function (modelDirective: Directive): ModelBinding { let shouldRender = true; let targetEventName = null; - let debounce: number|boolean = false; + let debounce: number | boolean = false; modelDirective.modifiers.forEach((modifier) => { switch (modifier.name) { case 'on': if (!modifier.value) { - throw new Error(`The "on" modifier in ${modelDirective.getString()} requires a value - e.g. on(change).`); + throw new Error( + `The "on" modifier in ${modelDirective.getString()} requires a value - e.g. on(change).` + ); } if (!['input', 'change'].includes(modifier.value)) { - throw new Error(`The "on" modifier in ${modelDirective.getString()} only accepts the arguments "input" or "change".`); + throw new Error( + `The "on" modifier in ${modelDirective.getString()} only accepts the arguments "input" or "change".` + ); } targetEventName = modifier.value; @@ -40,13 +44,13 @@ export default function(modelDirective: Directive): ModelBinding { } }); - const [ modelName, innerModelName ] = modelDirective.action.split(':'); + const [modelName, innerModelName] = modelDirective.action.split(':'); return { modelName, innerModelName: innerModelName || null, shouldRender, debounce, - targetEventName - } + targetEventName, + }; } diff --git a/src/LiveComponent/assets/src/Rendering/ChangingItemsTracker.ts b/src/LiveComponent/assets/src/Rendering/ChangingItemsTracker.ts index b6b22fee232..68940319c79 100644 --- a/src/LiveComponent/assets/src/Rendering/ChangingItemsTracker.ts +++ b/src/LiveComponent/assets/src/Rendering/ChangingItemsTracker.ts @@ -3,13 +3,13 @@ */ export default class { // e.g. a Map with key "color" & value { original: 'previousValue', new: 'newValue' }, - private changedItems: Map = new Map(); - private removedItems: Map = new Map(); + private changedItems: Map = new Map(); + private removedItems: Map = new Map(); /** * A "null" previousValue means the item was NOT previously present. */ - setItem(itemName: string, newValue: string, previousValue: string|null): void { + setItem(itemName: string, newValue: string, previousValue: string | null): void { if (this.removedItems.has(itemName)) { // this was previously removed @@ -24,7 +24,7 @@ export default class { if (this.changedItems.has(itemName)) { // this was previously changed - const originalRecord = this.changedItems.get(itemName) as { original: string, new: string }; + const originalRecord = this.changedItems.get(itemName) as { original: string; new: string }; if (originalRecord.original === newValue) { // it just reverted to its original value! this.changedItems.delete(itemName); @@ -41,11 +41,11 @@ export default class { this.changedItems.set(itemName, { original: previousValue, new: newValue }); } - removeItem(itemName: string, currentValue: string|null): void { + removeItem(itemName: string, currentValue: string | null): void { let trueOriginalValue = currentValue; if (this.changedItems.has(itemName)) { // this was previously changed, so we're just undoing that - const originalRecord = this.changedItems.get(itemName) as { original: string, new: string }; + const originalRecord = this.changedItems.get(itemName) as { original: string; new: string }; trueOriginalValue = originalRecord.original; this.changedItems.delete(itemName); @@ -61,7 +61,7 @@ export default class { } } - getChangedItems(): { name: string, value: string }[] { + getChangedItems(): { name: string; value: string }[] { return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value })); } diff --git a/src/LiveComponent/assets/src/Rendering/ElementChanges.ts b/src/LiveComponent/assets/src/Rendering/ElementChanges.ts index 66742225ed0..ed9df2ddf6a 100644 --- a/src/LiveComponent/assets/src/Rendering/ElementChanges.ts +++ b/src/LiveComponent/assets/src/Rendering/ElementChanges.ts @@ -22,7 +22,7 @@ export default class ElementChanges { } } - addStyle(styleName: string, newValue: string, originalValue: string|null) { + addStyle(styleName: string, newValue: string, originalValue: string | null) { this.styleChanges.setItem(styleName, newValue, originalValue); } @@ -30,11 +30,11 @@ export default class ElementChanges { this.styleChanges.removeItem(styleName, originalValue); } - addAttribute(attributeName: string, newValue: string, originalValue: string|null) { + addAttribute(attributeName: string, newValue: string, originalValue: string | null) { this.attributeChanges.setItem(attributeName, newValue, originalValue); } - removeAttribute(attributeName: string, originalValue: string|null) { + removeAttribute(attributeName: string, originalValue: string | null) { this.attributeChanges.removeItem(attributeName, originalValue); } @@ -46,7 +46,7 @@ export default class ElementChanges { return [...this.removedClasses]; } - getChangedStyles(): { name: string, value: string }[] { + getChangedStyles(): { name: string; value: string }[] { return this.styleChanges.getChangedItems(); } @@ -54,7 +54,7 @@ export default class ElementChanges { return this.styleChanges.getRemovedItems(); } - getChangedAttributes(): { name: string, value: string }[] { + getChangedAttributes(): { name: string; value: string }[] { return this.attributeChanges.getChangedItems(); } diff --git a/src/LiveComponent/assets/src/Rendering/ExternalMutationTracker.ts b/src/LiveComponent/assets/src/Rendering/ExternalMutationTracker.ts index 72341328aea..bc3ca917b88 100644 --- a/src/LiveComponent/assets/src/Rendering/ExternalMutationTracker.ts +++ b/src/LiveComponent/assets/src/Rendering/ExternalMutationTracker.ts @@ -45,8 +45,8 @@ export default class { } } - getChangedElement(element: Element): ElementChanges|null { - return this.changedElements.has(element) ? this.changedElements.get(element) as ElementChanges : null; + getChangedElement(element: Element): ElementChanges | null { + return this.changedElements.has(element) ? (this.changedElements.get(element) as ElementChanges) : null; } getAddedElements(): Element[] { @@ -103,16 +103,16 @@ export default class { // only process the first attribute mutation: it will have the // true original value, and we'll look at the true new value - if (!(handledAttributeMutations.get(element) as string[]).includes(mutation.attributeName as string)) { + if ( + !(handledAttributeMutations.get(element) as string[]).includes(mutation.attributeName as string) + ) { this.handleAttributeMutation(mutation); // add this attribute to the list of handled attributes - handledAttributeMutations.set( - element, [ - ...handledAttributeMutations.get(element) as string[], - mutation.attributeName as string - ]) - ; + handledAttributeMutations.set(element, [ + ...(handledAttributeMutations.get(element) as string[]), + mutation.attributeName as string, + ]); } break; } @@ -160,7 +160,7 @@ export default class { }); } - private handleAttributeMutation(mutation: MutationRecord):void { + private handleAttributeMutation(mutation: MutationRecord): void { const element = mutation.target as Element; if (!this.changedElements.has(element)) { @@ -213,7 +213,9 @@ export default class { const newValue = element.getAttribute('style') || ''; const newStyles = this.extractStyles(newValue); - const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]); + const addedOrChangedStyles = Object.keys(newStyles).filter( + (key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key] + ); const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]); addedOrChangedStyles.forEach((style) => { @@ -225,10 +227,7 @@ export default class { }); removedStyles.forEach((style) => { - elementChanges.removeStyle( - style, - previousStyles[style] - ); + elementChanges.removeStyle(style, previousStyles[style]); }); } @@ -255,10 +254,7 @@ export default class { return; } - elementChanges.removeAttribute( - attributeName, - mutation.oldValue as string - ); + elementChanges.removeAttribute(attributeName, mutation.oldValue as string); return; } @@ -270,11 +266,7 @@ export default class { return; } - elementChanges.addAttribute( - attributeName, - element.getAttribute(attributeName) as string, - mutation.oldValue - ); + elementChanges.addAttribute(attributeName, element.getAttribute(attributeName) as string, mutation.oldValue); } private extractStyles(styles: string): { [key: string]: string } { @@ -303,6 +295,6 @@ export default class { * re-renders, causing duplicate text. */ private isElementAddedByTranslation(element: Element): boolean { - return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;' + return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;'; } } diff --git a/src/LiveComponent/assets/test/Backend/RequestBuilder.test.ts b/src/LiveComponent/assets/test/Backend/RequestBuilder.test.ts index e7279fa6cd9..22f0e0498c4 100644 --- a/src/LiveComponent/assets/test/Backend/RequestBuilder.test.ts +++ b/src/LiveComponent/assets/test/Backend/RequestBuilder.test.ts @@ -7,12 +7,14 @@ describe('buildRequest', () => { { firstName: 'Ryan' }, [], { firstName: 'Kevin' }, - { 'child-component': {fingerprint: '123', tag: 'div' } }, + { 'child-component': { fingerprint: '123', tag: 'div' } }, {}, {} ); - expect(url).toEqual('/_components?existing_param=1&props=%7B%22firstName%22%3A%22Ryan%22%7D&updated=%7B%22firstName%22%3A%22Kevin%22%7D&children=%7B%22child-component%22%3A%7B%22fingerprint%22%3A%22123%22%2C%22tag%22%3A%22div%22%7D%7D'); + expect(url).toEqual( + '/_components?existing_param=1&props=%7B%22firstName%22%3A%22Ryan%22%7D&updated=%7B%22firstName%22%3A%22Kevin%22%7D&children=%7B%22child-component%22%3A%7B%22fingerprint%22%3A%22123%22%2C%22tag%22%3A%22div%22%7D%7D' + ); expect(fetchOptions.method).toEqual('GET'); expect(fetchOptions.headers).toEqual({ Accept: 'application/vnd.live-component+html', @@ -24,12 +26,14 @@ describe('buildRequest', () => { const builder = new RequestBuilder('/_components', 'post', '_the_csrf_token'); const { url, fetchOptions } = builder.buildRequest( { firstName: 'Ryan' }, - [{ - name: 'saveData', - args: { sendNotification: '1' }, - }], + [ + { + name: 'saveData', + args: { sendNotification: '1' }, + }, + ], { firstName: 'Kevin' }, - { 'child-component': {fingerprint: '123', tag: 'div' } }, + { 'child-component': { fingerprint: '123', tag: 'div' } }, {}, {} ); @@ -43,25 +47,30 @@ describe('buildRequest', () => { }); const body = fetchOptions.body; expect(body).toBeInstanceOf(FormData); - expect(body.get('data')).toEqual(JSON.stringify({ - props: { firstName: 'Ryan' }, - updated: { firstName: 'Kevin' }, - children: { 'child-component': { fingerprint: '123', tag: 'div' } }, - args: { sendNotification: '1' }, - })); + expect(body.get('data')).toEqual( + JSON.stringify({ + props: { firstName: 'Ryan' }, + updated: { firstName: 'Kevin' }, + children: { 'child-component': { fingerprint: '123', tag: 'div' } }, + args: { sendNotification: '1' }, + }) + ); }); it('sets basic data on POST request with batch actions', () => { const builder = new RequestBuilder('/_components', 'post', '_the_csrf_token'); const { url, fetchOptions } = builder.buildRequest( { firstName: 'Ryan' }, - [{ - name: 'saveData', - args: { sendNotification: '1' }, - }, { - name: 'saveData', - args: { sendNotification: '0' }, - }], + [ + { + name: 'saveData', + args: { sendNotification: '1' }, + }, + { + name: 'saveData', + args: { sendNotification: '0' }, + }, + ], { firstName: 'Kevin' }, {}, {}, @@ -72,17 +81,22 @@ describe('buildRequest', () => { expect(fetchOptions.method).toEqual('POST'); const body = fetchOptions.body; expect(body).toBeInstanceOf(FormData); - expect(body.get('data')).toEqual(JSON.stringify({ - props: { firstName: 'Ryan' }, - updated: { firstName: 'Kevin' }, - actions: [{ - name: 'saveData', - args: { sendNotification: '1' }, - }, { - name: 'saveData', - args: { sendNotification: '0' }, - }], - })); + expect(body.get('data')).toEqual( + JSON.stringify({ + props: { firstName: 'Ryan' }, + updated: { firstName: 'Kevin' }, + actions: [ + { + name: 'saveData', + args: { sendNotification: '1' }, + }, + { + name: 'saveData', + args: { sendNotification: '0' }, + }, + ], + }) + ); }); // when data is too long it makes a post request @@ -106,17 +120,19 @@ describe('buildRequest', () => { }); const body = fetchOptions.body; expect(body).toBeInstanceOf(FormData); - expect(body.get('data')).toEqual(JSON.stringify({ - props: { firstName: 'Ryan'.repeat(1000) }, - updated: { firstName: 'Kevin'.repeat(1000) }, - })); + expect(body.get('data')).toEqual( + JSON.stringify({ + props: { firstName: 'Ryan'.repeat(1000) }, + updated: { firstName: 'Kevin'.repeat(1000) }, + }) + ); }); it('makes a POST request when method is post', () => { const builder = new RequestBuilder('/_components', 'post', '_the_csrf_token'); const { url, fetchOptions } = builder.buildRequest( { - firstName: 'Ryan' + firstName: 'Ryan', }, [], { firstName: 'Kevin' }, @@ -134,34 +150,33 @@ describe('buildRequest', () => { }); const body = fetchOptions.body; expect(body).toBeInstanceOf(FormData); - expect(body.get('data')).toEqual(JSON.stringify({ - props: { - firstName: 'Ryan' - }, - updated: { firstName: 'Kevin' }, - })); + expect(body.get('data')).toEqual( + JSON.stringify({ + props: { + firstName: 'Ryan', + }, + updated: { firstName: 'Kevin' }, + }) + ); }); it('sends propsFromParent when specified', () => { const builder = new RequestBuilder('/_components?existing_param=1', 'get', '_the_csrf_token'); - const { url } = builder.buildRequest( - { firstName: 'Ryan' }, - [], - { firstName: 'Kevin' }, - { }, - { count: 5 }, - {} - ); + const { url } = builder.buildRequest({ firstName: 'Ryan' }, [], { firstName: 'Kevin' }, {}, { count: 5 }, {}); - expect(url).toEqual('/_components?existing_param=1&props=%7B%22firstName%22%3A%22Ryan%22%7D&updated=%7B%22firstName%22%3A%22Kevin%22%7D&propsFromParent=%7B%22count%22%3A5%7D'); + expect(url).toEqual( + '/_components?existing_param=1&props=%7B%22firstName%22%3A%22Ryan%22%7D&updated=%7B%22firstName%22%3A%22Kevin%22%7D&propsFromParent=%7B%22count%22%3A5%7D' + ); // do a POST const { fetchOptions } = builder.buildRequest( { firstName: 'Ryan' }, - [{ - name: 'saveData', - args: { sendNotification: '1' }, - }], + [ + { + name: 'saveData', + args: { sendNotification: '1' }, + }, + ], { firstName: 'Kevin' }, {}, { count: 5 }, @@ -170,12 +185,14 @@ describe('buildRequest', () => { const body = fetchOptions.body; expect(body).toBeInstanceOf(FormData); - expect(body.get('data')).toEqual(JSON.stringify({ - props: { firstName: 'Ryan' }, - updated: { firstName: 'Kevin' }, - propsFromParent: { count: 5 }, - args: { sendNotification: '1' }, - })); + expect(body.get('data')).toEqual( + JSON.stringify({ + props: { firstName: 'Ryan' }, + updated: { firstName: 'Kevin' }, + propsFromParent: { count: 5 }, + args: { sendNotification: '1' }, + }) + ); }); // Helper method for FileList mocking @@ -190,9 +207,9 @@ describe('buildRequest', () => { const file = blob; const fileList: FileList = { length: length, - item: () => file + item: () => file, }; - for (let i= 0; i < length; ++i) { + for (let i = 0; i < length; ++i) { fileList[i] = file; } return fileList; @@ -207,7 +224,7 @@ describe('buildRequest', () => { {}, {}, {}, - { file: getFileList()} + { file: getFileList() } ); expect(url).toEqual('/_components'); @@ -232,7 +249,7 @@ describe('buildRequest', () => { {}, {}, {}, - { 'file[]': getFileList(3), otherFile: getFileList()} + { 'file[]': getFileList(3), otherFile: getFileList() } ); expect(url).toEqual('/_components'); diff --git a/src/LiveComponent/assets/test/Component/index.test.ts b/src/LiveComponent/assets/test/Component/index.test.ts index 6ab1399c6b7..1d72ee0adcf 100644 --- a/src/LiveComponent/assets/test/Component/index.test.ts +++ b/src/LiveComponent/assets/test/Component/index.test.ts @@ -1,5 +1,5 @@ import Component, { proxifyComponent } from '../../src/Component'; -import type {BackendAction, BackendInterface} from '../../src/Backend/Backend'; +import type { BackendAction, BackendInterface } from '../../src/Backend/Backend'; import BackendRequest from '../../src/Backend/BackendRequest'; import { Response } from 'node-fetch'; import { waitFor } from '@testing-library/dom'; @@ -7,10 +7,10 @@ import type BackendResponse from '../../src/Backend/BackendResponse'; import { noopElementDriver } from '../tools'; interface MockBackend extends BackendInterface { - actions: BackendAction[], + actions: BackendAction[]; } -const makeTestComponent = (): { component: Component, backend: MockBackend } => { +const makeTestComponent = (): { component: Component; backend: MockBackend } => { const backend: MockBackend = { actions: [], makeRequest(data: any, actions: BackendAction[]): BackendRequest { @@ -21,9 +21,9 @@ const makeTestComponent = (): { component: Component, backend: MockBackend } => new Promise((resolve) => resolve(new Response('
'))), [], [] - ) - } - } + ); + }, + }; const component = new Component( document.createElement('div'), @@ -32,26 +32,28 @@ const makeTestComponent = (): { component: Component, backend: MockBackend } => [], null, backend, - new noopElementDriver(), + new noopElementDriver() ); return { component, - backend - } -} + backend, + }; +}; describe('Component class', () => { describe('set() method', () => { it('returns a Promise that eventually resolves', async () => { const { component } = makeTestComponent(); - let backendResponse: BackendResponse|null = null; + let backendResponse: BackendResponse | null = null; // set model but no re-render const promise = component.set('firstName', 'Ryan', false); // when this promise IS finally resolved, set the flag to true - promise.then((response) => { backendResponse = response }); + promise.then((response) => { + backendResponse = response; + }); // it should not have happened yet expect(backendResponse).toBeNull(); @@ -69,18 +71,20 @@ describe('Component class', () => { // setting nested - totally ok component.set('product.name', 'Ryan', false); - expect(() => { component.set('notARealModel', 'Ryan', false) }).toThrow('Invalid model name "notARealModel"'); + expect(() => { + component.set('notARealModel', 'Ryan', false); + }).toThrow('Invalid model name "notARealModel"'); }); }); describe('Proxy wrapper', () => { - const makeDummyComponent = (): { proxy: Component, backend: MockBackend } => { - const { backend, component} = makeTestComponent(); + const makeDummyComponent = (): { proxy: Component; backend: MockBackend } => { + const { backend, component } = makeTestComponent(); return { proxy: proxifyComponent(component), - backend - } - } + backend, + }; + }; it('forwards real property gets', () => { const { proxy } = makeDummyComponent(); @@ -120,7 +124,7 @@ describe('Component class', () => { // ugly: the action delays for 0ms, so we just need a TINy // delay here before we start asserting - await (new Promise(resolve => setTimeout(resolve, 5))); + await new Promise((resolve) => setTimeout(resolve, 5)); expect(backend.actions).toHaveLength(1); expect(backend.actions[0].name).toBe('save'); expect(backend.actions[0].args).toEqual({ foo: 'bar', secondArg: 'secondValue' }); diff --git a/src/LiveComponent/assets/test/ComponentRegistry.test.ts b/src/LiveComponent/assets/test/ComponentRegistry.test.ts index 7213fc866bd..e47cc14e394 100644 --- a/src/LiveComponent/assets/test/ComponentRegistry.test.ts +++ b/src/LiveComponent/assets/test/ComponentRegistry.test.ts @@ -1,10 +1,5 @@ import Component from '../src/Component'; -import { - registerComponent, - resetRegistry, - getComponent, - findComponents, -} from '../src/ComponentRegistry'; +import { registerComponent, resetRegistry, getComponent, findComponents } from '../src/ComponentRegistry'; import BackendRequest from '../src/Backend/BackendRequest'; import type { BackendInterface } from '../src/Backend/Backend'; import { Response } from 'node-fetch'; @@ -18,19 +13,11 @@ const createComponent = (element: HTMLElement, name = 'foo-component'): Componen new Promise((resolve) => resolve(new Response(''))), [], [] - ) - } - } - - return new Component( - element, - name, - {}, - [], - null, - backend, - new noopElementDriver(), - ); + ); + }, + }; + + return new Component(element, name, {}, [], null, backend, new noopElementDriver()); }; describe('ComponentRegistry', () => { diff --git a/src/LiveComponent/assets/test/Directive/directives_parser.test.ts b/src/LiveComponent/assets/test/Directive/directives_parser.test.ts index dd410382dce..b0488b88583 100644 --- a/src/LiveComponent/assets/test/Directive/directives_parser.test.ts +++ b/src/LiveComponent/assets/test/Directive/directives_parser.test.ts @@ -1,4 +1,4 @@ -import {type Directive, parseDirectives} from '../../src/Directive/directives_parser'; +import { type Directive, parseDirectives } from '../../src/Directive/directives_parser'; const assertDirectiveEquals = (actual: Directive, expected: any) => { // normalize this so that it doesn't trip up the comparison @@ -7,7 +7,7 @@ const assertDirectiveEquals = (actual: Directive, expected: any) => { expected.getString = getString; expect(actual).toEqual(expected); -} +}; describe('directives parser', () => { it('parses no attribute value', () => { @@ -30,7 +30,7 @@ describe('directives parser', () => { action: 'hide', args: [], modifiers: [], - }) + }); }); it('parses an action with a simple argument', () => { @@ -40,7 +40,7 @@ describe('directives parser', () => { action: 'addClass', args: ['opacity-50'], modifiers: [], - }) + }); }); it('parses an action with one argument with a space', () => { @@ -50,7 +50,7 @@ describe('directives parser', () => { action: 'addClass', args: ['opacity-50 disabled'], modifiers: [], - }) + }); }); it('parses an action with multiple, unnamed arguments', () => { @@ -61,7 +61,7 @@ describe('directives parser', () => { // space between arguments is trimmed args: ['opacity-50', 'disabled'], modifiers: [], - }) + }); }); it('parses multiple actions simple', () => { @@ -71,12 +71,12 @@ describe('directives parser', () => { action: 'addClass', args: ['opacity-50'], modifiers: [], - }) + }); assertDirectiveEquals(directives[1], { action: 'addAttribute', args: ['disabled'], modifiers: [], - }) + }); }); it('parses multiple actions with multiple arguments', () => { @@ -86,17 +86,17 @@ describe('directives parser', () => { action: 'hide', args: [], modifiers: [], - }) + }); assertDirectiveEquals(directives[1], { action: 'addClass', args: ['opacity-50 disabled'], modifiers: [], - }) + }); assertDirectiveEquals(directives[2], { action: 'addAttribute', args: ['disabled'], modifiers: [], - }) + }); }); it('parses simple modifiers', () => { @@ -105,10 +105,8 @@ describe('directives parser', () => { assertDirectiveEquals(directives[0], { action: 'addClass', args: ['disabled'], - modifiers: [ - { name: 'delay', value: null } - ], - }) + modifiers: [{ name: 'delay', value: null }], + }); }); it('parses modifiers with argument', () => { @@ -117,10 +115,8 @@ describe('directives parser', () => { assertDirectiveEquals(directives[0], { action: 'addClass', args: ['disabled'], - modifiers: [ - { name: 'delay', value: '400' }, - ], - }) + modifiers: [{ name: 'delay', value: '400' }], + }); }); it('parses multiple modifiers', () => { @@ -133,32 +129,32 @@ describe('directives parser', () => { { name: 'prevent', value: null }, { name: 'debounce', value: '400' }, ], - }) + }); }); describe('errors on syntax errors', () => { it('missing ending )', () => { expect(() => { parseDirectives('addClass(opacity-50'); - }).toThrow('Did you forget to add a closing ")" after "addClass"?') + }).toThrow('Did you forget to add a closing ")" after "addClass"?'); }); it('missing ending before next action', () => { expect(() => { parseDirectives('addClass(opacity-50 hide'); - }).toThrow('Did you forget to add a closing ")" after "addClass"?') + }).toThrow('Did you forget to add a closing ")" after "addClass"?'); }); it('no space between actions', () => { expect(() => { parseDirectives('addClass(opacity-50)hide'); - }).toThrow('Missing space after addClass()') + }).toThrow('Missing space after addClass()'); }); it('modifier cannot have multiple arguments', () => { expect(() => { parseDirectives('debounce(10, 20)|save'); - }).toThrow('The modifier "debounce()" does not support multiple arguments.') + }).toThrow('The modifier "debounce()" does not support multiple arguments.'); }); }); }); diff --git a/src/LiveComponent/assets/test/Directive/get_model_binding.test.ts b/src/LiveComponent/assets/test/Directive/get_model_binding.test.ts index b251cc75641..4c4ff6c1f9f 100644 --- a/src/LiveComponent/assets/test/Directive/get_model_binding.test.ts +++ b/src/LiveComponent/assets/test/Directive/get_model_binding.test.ts @@ -1,5 +1,5 @@ import getModelBinding from '../../src/Directive/get_model_binding'; -import {parseDirectives} from '../../src/Directive/directives_parser'; +import { parseDirectives } from '../../src/Directive/directives_parser'; describe('get_model_binding', () => { it('returns correctly with simple directive', () => { diff --git a/src/LiveComponent/assets/test/Rendering/ChangingItemsTracker.test.ts b/src/LiveComponent/assets/test/Rendering/ChangingItemsTracker.test.ts index f4c5a4fdc03..fbf3ed0971c 100644 --- a/src/LiveComponent/assets/test/Rendering/ChangingItemsTracker.test.ts +++ b/src/LiveComponent/assets/test/Rendering/ChangingItemsTracker.test.ts @@ -7,7 +7,7 @@ describe('ChangingItemsTracker', () => { items.setItem('color', 'blue', 'red'); items.setItem('color', 'green', 'blue'); expect(items.getChangedItems()).toHaveLength(1); - expect(items.getChangedItems()[0]).toEqual({ name: 'color', value: 'green'}); + expect(items.getChangedItems()[0]).toEqual({ name: 'color', value: 'green' }); expect(items.getRemovedItems()).toHaveLength(0); }); @@ -16,7 +16,7 @@ describe('ChangingItemsTracker', () => { items.setItem('color', 'blue', null); items.setItem('color', 'green', 'blue'); expect(items.getChangedItems()).toHaveLength(1); - expect(items.getChangedItems()[0]).toEqual({ name: 'color', value: 'green'}); + expect(items.getChangedItems()[0]).toEqual({ name: 'color', value: 'green' }); expect(items.getRemovedItems()).toHaveLength(0); }); diff --git a/src/LiveComponent/assets/test/Rendering/ElementChanges.test.ts b/src/LiveComponent/assets/test/Rendering/ElementChanges.test.ts index 33494d61b04..d48bfb13186 100644 --- a/src/LiveComponent/assets/test/Rendering/ElementChanges.test.ts +++ b/src/LiveComponent/assets/test/Rendering/ElementChanges.test.ts @@ -9,7 +9,7 @@ describe('ElementChanges', () => { changes.removeClass('new-class2'); changes.addClass('new-class3'); - changes.removeClass('removed-class') + changes.removeClass('removed-class'); expect(changes.getAddedClasses()).toHaveLength(2); expect(changes.getAddedClasses()).toContain('new-class1'); @@ -29,8 +29,8 @@ describe('ElementChanges', () => { changes.removeStyle('margin', '10px'); expect(changes.getChangedStyles()).toHaveLength(2); - expect(changes.getChangedStyles()).toContainEqual({ name: 'color', value: 'green'}); - expect(changes.getChangedStyles()).toContainEqual({ name: 'display', value: 'none'}); + expect(changes.getChangedStyles()).toContainEqual({ name: 'color', value: 'green' }); + expect(changes.getChangedStyles()).toContainEqual({ name: 'display', value: 'none' }); expect(changes.getRemovedStyles()).toHaveLength(1); expect(changes.getRemovedStyles()).toContain('margin'); }); @@ -50,8 +50,8 @@ describe('ElementChanges', () => { changes.removeAttribute('disabled', ''); expect(changes.getChangedAttributes()).toHaveLength(2); - expect(changes.getChangedAttributes()).toContainEqual({ name: 'data-foo', value: 'qux'}); - expect(changes.getChangedAttributes()).toContainEqual({ name: 'align', value: 'none'}); + expect(changes.getChangedAttributes()).toContainEqual({ name: 'data-foo', value: 'qux' }); + expect(changes.getChangedAttributes()).toContainEqual({ name: 'align', value: 'none' }); expect(changes.getRemovedAttributes()).toHaveLength(1); expect(changes.getRemovedAttributes()).toContain('data-bar'); }); diff --git a/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts b/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts index ece626001a9..f92f910fa05 100644 --- a/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts +++ b/src/LiveComponent/assets/test/Rendering/ExternalMutationTracker.test.ts @@ -7,15 +7,15 @@ const mountElement = (html: string): HTMLElement => { document.body.appendChild(element); return element; -} +}; -const createTracker = (html: string): { element: HTMLElement, tracker: ExternalMutationTracker } => { +const createTracker = (html: string): { element: HTMLElement; tracker: ExternalMutationTracker } => { const element = mountElement(html); const tracker = new ExternalMutationTracker(element, () => true); tracker.start(); return { element, tracker }; -} +}; /* * This is a hack to get around the fact that MutationObserver doesn't fire synchronously. @@ -24,13 +24,13 @@ const shortTimeout = (): Promise => { return new Promise((resolve) => { setTimeout(resolve, 10); }); -} +}; describe('ExternalMutationTracker', () => { it('can track generic attribute changes', async () => { const { element, tracker } = createTracker(`
Text inside!
- `) + `); // change x2 element.setAttribute('id', 'middle-id'); @@ -69,7 +69,7 @@ describe('ExternalMutationTracker', () => { it('can track style changes', async () => { const { element, tracker } = createTracker(` - `) + `); // change x2 element.style.display = 'block'; @@ -108,13 +108,13 @@ describe('ExternalMutationTracker', () => { it('can track class changes', async () => { const { element, tracker } = createTracker(`
Text inside!
- `) + `); // remove then add back element.classList.remove('second-class'); element.classList.add('second-class'); // add new (with some whitespace to be sneaky) - element.setAttribute('class', ` ${element.getAttribute('class')} \n new-class `) + element.setAttribute('class', ` ${element.getAttribute('class')} \n new-class `); // remove element.classList.remove('first-class'); // add then remove @@ -144,7 +144,7 @@ describe('ExternalMutationTracker', () => { third-class " >Text inside! - `) + `); element.classList.remove('second-class'); element.classList.add('new-class'); @@ -170,7 +170,7 @@ describe('ExternalMutationTracker', () => { the span 1 the span 2 - `) + `); const span1 = document.getElementById('original-span1') as HTMLElement; const span2 = document.getElementById('original-span2') as HTMLElement; @@ -214,7 +214,7 @@ describe('ExternalMutationTracker', () => { // still just 1 element added expect(tracker.getAddedElements()).toHaveLength(1); expect(tracker.changedElementsCount).toBe(1); - expect(tracker.getChangedElement(element)).not.toBeNull() + expect(tracker.getChangedElement(element)).not.toBeNull(); }); it('ignores changes based on the callback', async () => { diff --git a/src/LiveComponent/assets/test/UnsyncedInputContainer.test.ts b/src/LiveComponent/assets/test/UnsyncedInputContainer.test.ts index b501bb76064..c7b614de3b7 100644 --- a/src/LiveComponent/assets/test/UnsyncedInputContainer.test.ts +++ b/src/LiveComponent/assets/test/UnsyncedInputContainer.test.ts @@ -36,7 +36,7 @@ describe('UnsyncedInputContainer', () => { container.add(element3, 'some_model3'); container.markModelAsSynced('some_model2'); - expect(container.getUnsyncedModelNames()).toEqual(['some_model3']) + expect(container.getUnsyncedModelNames()).toEqual(['some_model3']); }); it('resetUnsyncedFields removes all model fields except those unsynced', () => { diff --git a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts b/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts index 8d1f457ff5e..4bd12b3a907 100644 --- a/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts +++ b/src/LiveComponent/assets/test/Util/getElementAsTagText.test.ts @@ -5,12 +5,12 @@ describe('getElementAsTagText', () => { it('returns self-closing tag correctly', () => { const element = htmlToElement(''); - expect(getElementAsTagText(element)).toEqual('') + expect(getElementAsTagText(element)).toEqual(''); }); it('returns tag text without the innerHTML', () => { const element = htmlToElement('
Name:
'); - expect(getElementAsTagText(element)).toEqual('
') + expect(getElementAsTagText(element)).toEqual('
'); }); }); diff --git a/src/LiveComponent/assets/test/ValueStore.test.ts b/src/LiveComponent/assets/test/ValueStore.test.ts index 9faa081ce5d..762e8a5223d 100644 --- a/src/LiveComponent/assets/test/ValueStore.test.ts +++ b/src/LiveComponent/assets/test/ValueStore.test.ts @@ -32,7 +32,7 @@ describe('ValueStore', () => { { props: { user: 111, - 'user.FirstName': 'Ryan' + 'user.FirstName': 'Ryan', }, name: 'user', expected: 111, @@ -55,7 +55,7 @@ describe('ValueStore', () => { }, { props: { firstName: 'Ryan' }, - updated: [ { prop: 'firstName', value: 'Kevin' }], + updated: [{ prop: 'firstName', value: 'Kevin' }], name: 'firstName', expected: 'Kevin', }, @@ -65,7 +65,7 @@ describe('ValueStore', () => { firstName: 'Ryan', }, }, - updated: [ { prop: 'user.firstName', value: 'Kevin' }], + updated: [{ prop: 'user.firstName', value: 'Kevin' }], name: 'user.firstName', expected: 'Kevin', }, @@ -104,7 +104,7 @@ describe('ValueStore', () => { { props: { user: 5, - 'user.firstName': 'Ryan' + 'user.firstName': 'Ryan', }, name: 'user.firstName', expected: true, @@ -121,7 +121,7 @@ describe('ValueStore', () => { { props: { user: 111, - 'user.firstName': 'Ryan' + 'user.firstName': 'Ryan', }, name: 'user', expected: true, @@ -179,7 +179,7 @@ describe('ValueStore', () => { user: { firstName: 'Ryan', lastName: 'Weaver', - } + }, }, set: 'user', to: { @@ -232,11 +232,8 @@ describe('ValueStore', () => { expect(store.get('firstName')).toEqual('Wouter'); }); - it('getOriginalProps() returns props', () => { - const container = new ValueStore( - { city: 'Grand Rapids', user: 'Kevin', product: 5 }, - ); + const container = new ValueStore({ city: 'Grand Rapids', user: 'Kevin', product: 5 }); expect(container.getOriginalProps()).toEqual({ city: 'Grand Rapids', user: 'Kevin', product: 5 }); }); @@ -293,7 +290,7 @@ describe('ValueStore', () => { user: { firstName: 'Ryan', lastName: 'Weaver', - } + }, }, newProps: { user: { diff --git a/src/LiveComponent/assets/test/controller/action.test.ts b/src/LiveComponent/assets/test/controller/action.test.ts index cb0a654f2f4..283e2515b51 100644 --- a/src/LiveComponent/assets/test/controller/action.test.ts +++ b/src/LiveComponent/assets/test/controller/action.test.ts @@ -14,16 +14,19 @@ import userEvent from '@testing-library/user-event'; describe('LiveController Action Tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('sends an action and renders the result', async () => { - const test = await createTest({ comment: 'great turtles!', isSaved: false }, (data: any) => ` + const test = await createTest( + { comment: 'great turtles!', isSaved: false }, + (data: any) => `
${data.isSaved ? 'Comment Saved!' : ''}
- `); + ` + ); test.expectsAjaxCall() .expectActionCalled('save') @@ -38,7 +41,9 @@ describe('LiveController Action Tests', () => { }); it('immediately sends an action, includes debouncing model updates and cancels those debounce renders', async () => { - const test = await createTest({ comment: '', isSaved: false }, (data: any) => ` + const test = await createTest( + { comment: '', isSaved: false }, + (data: any) => `
@@ -46,7 +51,8 @@ describe('LiveController Action Tests', () => {
- `); + ` + ); // JUST the POST request: no other GET requests test.expectsAjaxCall() @@ -66,11 +72,13 @@ describe('LiveController Action Tests', () => { await waitFor(() => expect(test.element).toHaveTextContent('Comment Saved!')); // wait long enough for the debounced model update to happen, if it wasn't canceled - await (new Promise(resolve => setTimeout(resolve, 50))); + await new Promise((resolve) => setTimeout(resolve, 50)); }); it('Sends action with named args', async () => { - const test = await createTest({ isSaved: false}, (data: any) => ` + const test = await createTest( + { isSaved: false }, + (data: any) => `
${data.isSaved ? 'Component Saved!' : ''} @@ -82,23 +90,26 @@ describe('LiveController Action Tests', () => { data-live-c-param="banana" >Send named args
- `); + ` + ); // ONLY a post is sent, not a re-render GET test.expectsAjaxCall() - .expectActionCalled('sendNamedArgs', {a: 1, b: 2, c: 'banana'}) + .expectActionCalled('sendNamedArgs', { a: 1, b: 2, c: 'banana' }) .serverWillChangeProps((data: any) => { // server marks component as "saved" data.isSaved = true; }); - getByText(test.element, 'Send named args').click(); + getByText(test.element, 'Send named args').click(); - await waitFor(() => expect(test.element).toHaveTextContent('Component Saved!')); + await waitFor(() => expect(test.element).toHaveTextContent('Component Saved!')); }); it('sends an action but allows for the model to be updated', async () => { - const test = await createTest({ food: '' }, (data: any) => ` + const test = await createTest( + { food: '' }, + (data: any) => `
@@ -137,7 +149,8 @@ describe('LiveController Action Tests', () => {
- `); + ` + ); // ONLY a post is sent, not a re-render GET test.expectsAjaxCall() @@ -152,9 +165,8 @@ describe('LiveController Action Tests', () => { // which will take 100ms. So, don't start expecting it until nearly then // but after the model debounce setTimeout(() => { - test.expectsAjaxCall() - .expectUpdatedData({comment: 'donut holes'}); - }, 75) + test.expectsAjaxCall().expectUpdatedData({ comment: 'donut holes' }); + }, 75); // save first, then type into the box getByText(test.element, 'Save').click(); @@ -171,13 +183,16 @@ describe('LiveController Action Tests', () => { }); it('batches multiple actions together', async () => { - const test = await createTest({ isSaved: false }, (data: any) => ` + const test = await createTest( + { isSaved: false }, + (data: any) => `
${data.isSaved ? 'Component Saved!' : ''}
- `); + ` + ); // 1 request with all 3 actions test.expectsAjaxCall() diff --git a/src/LiveComponent/assets/test/controller/basic.test.ts b/src/LiveComponent/assets/test/controller/basic.test.ts index 43d60955808..bfe91598a9c 100644 --- a/src/LiveComponent/assets/test/controller/basic.test.ts +++ b/src/LiveComponent/assets/test/controller/basic.test.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import {createTest, initComponent, shutdownTests, startStimulus} from '../tools'; +import { createTest, initComponent, shutdownTests, startStimulus } from '../tools'; import { htmlToElement } from '../../src/dom_utils'; import Component from '../../src/Component'; import { getComponent } from '../../src/live_controller'; @@ -15,7 +15,7 @@ import { findComponents } from '../../src/ComponentRegistry'; describe('LiveController Basic Tests', () => { afterEach(() => { - shutdownTests() + shutdownTests(); }); it('dispatches connect event', async () => { @@ -24,7 +24,7 @@ describe('LiveController Basic Tests', () => { let eventTriggered = false; container.addEventListener('live:connect', () => { eventTriggered = true; - }) + }); const { element } = await startStimulus(container); // smoke test @@ -33,9 +33,12 @@ describe('LiveController Basic Tests', () => { }); it('creates the Component object', async () => { - const test = await createTest({ firstName: 'Ryan' }, (data: any) => ` + const test = await createTest( + { firstName: 'Ryan' }, + (data: any) => `
- `); + ` + ); expect(test.component).toBeInstanceOf(Component); expect(test.component.defaultDebounce).toEqual(115); diff --git a/src/LiveComponent/assets/test/controller/child-model.test.ts b/src/LiveComponent/assets/test/controller/child-model.test.ts index 356c7529dc6..50b26d05715 100644 --- a/src/LiveComponent/assets/test/controller/child-model.test.ts +++ b/src/LiveComponent/assets/test/controller/child-model.test.ts @@ -8,42 +8,47 @@ */ import { createTest, initComponent, shutdownTests } from '../tools'; -import {getByTestId, waitFor} from '@testing-library/dom'; +import { getByTestId, waitFor } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; describe('Component parent -> child data-model binding tests', () => { afterEach(() => { shutdownTests(); - }) + }); // updating stops when child is removed, restarts after // more complex foo:bar model binding works // multiple model bindings work it('updates parent model in simple setup', async () => { - const test = await createTest({ foodName: ''}, (data: any) => ` + const test = await createTest( + { foodName: '' }, + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); test.expectsAjaxCall() .expectUpdatedData({ foodName: 'ice cream' }) // mimic that the data on the child props have not changed, so we // render a simple placeholder - .willReturn((data: any) => ` + .willReturn( + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); // type into the child component await userEvent.type(test.queryByDataModel('value'), 'ice cream'); @@ -54,29 +59,34 @@ describe('Component parent -> child data-model binding tests', () => { }); it('will default to "value" for the model name', async () => { - const test = await createTest({ foodName: ''}, (data: any) => ` + const test = await createTest( + { foodName: '' }, + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); test.expectsAjaxCall() .expectUpdatedData({ foodName: 'ice cream' }) // mimic that the data on the child props have not changed, so we // render a simple placeholder - .willReturn((data: any) => ` + .willReturn( + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); // type into the child component await userEvent.type(test.queryByDataModel('value'), 'ice cream'); @@ -87,18 +97,21 @@ describe('Component parent -> child data-model binding tests', () => { }); it('considers modifiers when updating parent model', async () => { - const test = await createTest({ foodName: ''}, (data: any) => ` + const test = await createTest( + { foodName: '' }, + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); // type into the child component await userEvent.type(test.queryByDataModel('value'), 'ice cream'); @@ -108,34 +121,39 @@ describe('Component parent -> child data-model binding tests', () => { // but it never triggers an Ajax call, because the norender modifier expect(test.element).not.toHaveAttribute('busy'); // wait for a potential Ajax call to start - await (new Promise(resolve => setTimeout(resolve, 50))); + await new Promise((resolve) => setTimeout(resolve, 50)); expect(test.element).not.toHaveAttribute('busy'); }); it('start and stops model binding as child is added/removed', async () => { - const test = await createTest({ foodName: ''}, (data: any) => ` + const test = await createTest( + { foodName: '' }, + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); test.expectsAjaxCall() .expectUpdatedData({ foodName: 'ice cream' }) // mimic that the data on the child props have not changed, so we // render a simple placeholder - .willReturn((data: any) => ` + .willReturn( + (data: any) => `
Food Name ${data.foodName}
- `); + ` + ); // type into the child component const inputElement = test.queryByDataModel('value'); @@ -152,7 +170,7 @@ describe('Component parent -> child data-model binding tests', () => { // type into the child component await userEvent.type(inputElement, ' sandwich'); // wait for a potential Ajax call to start - await (new Promise(resolve => setTimeout(resolve, 50))); + await new Promise((resolve) => setTimeout(resolve, 50)); expect(test.element).not.toHaveAttribute('busy'); }); }); diff --git a/src/LiveComponent/assets/test/controller/child.test.ts b/src/LiveComponent/assets/test/controller/child.test.ts index 25d6afc9d5f..8d091a5c53f 100644 --- a/src/LiveComponent/assets/test/controller/child.test.ts +++ b/src/LiveComponent/assets/test/controller/child.test.ts @@ -14,7 +14,7 @@ import { shutdownTests, getComponent, dataToJsonAttribute, - getStimulusApplication + getStimulusApplication, } from '../tools'; import { getByTestId, waitFor } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; @@ -24,79 +24,87 @@ import { Controller } from '@hotwired/stimulus'; describe('Component parent -> child initialization and rendering tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('sends a map of child fingerprints on re-render', async () => { - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
-
Child1
-
Child2
+
Child1
+
Child2
- `); + ` + ); - test.expectsAjaxCall() - .expectChildFingerprints({ - 'the-child-id1': { fingerprint: 'child-fingerprint1', tag: 'div' }, - 'the-child-id2': { fingerprint: 'child-fingerprint2', tag: 'div' }, - }); + test.expectsAjaxCall().expectChildFingerprints({ + 'the-child-id1': { fingerprint: 'child-fingerprint1', tag: 'div' }, + 'the-child-id2': { fingerprint: 'child-fingerprint2', tag: 'div' }, + }); test.component.render(); await waitFor(() => expect(test.element).toHaveAttribute('busy')); }); it('removes missing child component on re-render', async () => { - const test = await createTest({renderChild: true}, (data: any) => ` + const test = await createTest( + { renderChild: true }, + (data: any) => `
- ${data.renderChild - ? `
Child Component
` - : '' + ${ + data.renderChild + ? `
Child Component
` + : '' }
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.renderChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.renderChild = false; + }); - expect(test.element).toHaveTextContent('Child Component') + expect(test.element).toHaveTextContent('Child Component'); expect(findChildren(test.component).length).toEqual(1); test.component.render(); // wait for child to disappear await waitFor(() => expect(test.element).toHaveAttribute('busy')); await waitFor(() => expect(test.element).not.toHaveAttribute('busy')); - expect(test.element).not.toHaveTextContent('Child Component') + expect(test.element).not.toHaveTextContent('Child Component'); expect(findChildren(test.component).length).toEqual(0); }); it('adds new child component on re-render', async () => { - const test = await createTest({renderChild: false}, (data: any) => ` + const test = await createTest( + { renderChild: false }, + (data: any) => `
- ${data.renderChild - ? `
Child Component
` - : '' - } + ${ + data.renderChild + ? `
Child Component
` + : '' + }
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.renderChild = true; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.renderChild = true; + }); - expect(test.element).not.toHaveTextContent('Child Component') + expect(test.element).not.toHaveTextContent('Child Component'); expect(findChildren(test.component).length).toEqual(0); test.component.render(); // wait for child to disappear await waitFor(() => expect(test.element).toHaveAttribute('busy')); await waitFor(() => expect(test.element).not.toHaveAttribute('busy')); - expect(test.element).toHaveTextContent('Child Component') + expect(test.element).toHaveTextContent('Child Component'); expect(findChildren(test.component).length).toEqual(1); }); it('new child marked as data-live-preserve is ignored except for new attributes', async () => { const originalChild = ` -
+
Original Child Component
`; @@ -106,31 +114,33 @@ describe('Component parent -> child initialization and rendering tests', () => {
`; - const test = await createTest({useOriginalChild: true}, (data: any) => ` + const test = await createTest( + { useOriginalChild: true }, + (data: any) => `
${data.useOriginalChild ? originalChild : updatedChild}
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.useOriginalChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.useOriginalChild = false; + }); - expect(test.element).toHaveTextContent('Original Child Component') + expect(test.element).toHaveTextContent('Original Child Component'); test.component.render(); // wait for Ajax call await waitFor(() => expect(test.element).toHaveAttribute('busy')); await waitFor(() => expect(test.element).not.toHaveAttribute('busy')); // child component is STILL here: the new rendering was ignored - expect(test.element).toHaveTextContent('Original Child Component') + expect(test.element).toHaveTextContent('Original Child Component'); expect(test.element).toContainHTML('data-new="bar"'); expect(test.element).not.toContainHTML('data-live-preserve'); }); it('data-live-preserve child in same location is not removed/re-added to the DOM', async () => { const originalChild = ` -
+
Original Child Component
@@ -139,22 +149,27 @@ describe('Component parent -> child initialization and rendering tests', () => {
`; - const test = await createTest({useOriginalChild: true}, (data: any) => ` + const test = await createTest( + { useOriginalChild: true }, + (data: any) => `
${data.useOriginalChild ? originalChild : updatedChild}
- `); - - getStimulusApplication().register('track-connect', class extends Controller { - disconnect() { - this.element.setAttribute('disconnected', ''); + ` + ); + + getStimulusApplication().register( + 'track-connect', + class extends Controller { + disconnect() { + this.element.setAttribute('disconnected', ''); + } } - }); + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.useOriginalChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.useOriginalChild = false; + }); await test.component.render(); // sanity check that the child is there @@ -165,7 +180,7 @@ describe('Component parent -> child initialization and rendering tests', () => { it('data-live-preserve element moved correctly when position changes and old element morphed into different element', async () => { const originalChild = ` -
+
Original Child Component
@@ -176,17 +191,19 @@ describe('Component parent -> child initialization and rendering tests', () => { // when morphing original -> updated, the outer div (which was the child) // will be morphed into a normal div - const test = await createTest({useOriginalChild: true}, (data: any) => ` + const test = await createTest( + { useOriginalChild: true }, + (data: any) => `
${data.useOriginalChild ? originalChild : ''} ${data.useOriginalChild ? '' : `
${updatedChild}
`}
- `) + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.useOriginalChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.useOriginalChild = false; + }); const childElement = getByTestId(test.element, 'child-component'); await test.component.render(); @@ -200,7 +217,7 @@ describe('Component parent -> child initialization and rendering tests', () => { const childTemplate = (data: any) => `
Full Name: ${data.toUppercase ? data.fullName.toUpperCase() : data.fullName} @@ -213,19 +230,23 @@ describe('Component parent -> child initialization and rendering tests', () => { id="the-child-id" data-live-fingerprint-value="updated fingerprint" data-live-preserve - data-live-props-updated-from-parent-value="${dataToJsonAttribute({toUppercase: true})}" + data-live-props-updated-from-parent-value="${dataToJsonAttribute({ toUppercase: true })}" >
`; - const test = await createTest({useOriginalChild: true}, (data: any) => ` + const test = await createTest( + { useOriginalChild: true }, + (data: any) => `
Using Original child: ${data.useOriginalChild ? 'yes' : 'no'} - ${data.useOriginalChild - ? childTemplate({ fullName: 'Ryan', toUppercase: false }) - : childReturnedFromParentCall - } + ${ + data.useOriginalChild + ? childTemplate({ fullName: 'Ryan', toUppercase: false }) + : childReturnedFromParentCall + }
- `); + ` + ); const childComponent = getComponent(getByTestId(test.element, 'child-component')); // just used to mock the Ajax call @@ -253,10 +274,9 @@ describe('Component parent -> child initialization and rendering tests', () => { userEvent.type(childTest.queryByDataModel('fullName'), ' Weaver'); // C) Re-render the parent - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.useOriginalChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.useOriginalChild = false; + }); test.component.render(); // wait for parent Ajax call to start await waitFor(() => expect(test.element).toHaveAttribute('busy')); @@ -264,7 +284,8 @@ describe('Component parent -> child initialization and rendering tests', () => { // E) Expect the child to re-render // after the parent Ajax call has finished, but shortly before it's // done processing, the child component should start its own Ajax call - childTest.expectsAjaxCall() + childTest + .expectsAjaxCall() // expect the modified firstName data // expect the new prop .expectUpdatedData({ fullName: 'Ryan Weaver' }) @@ -274,7 +295,7 @@ describe('Component parent -> child initialization and rendering tests', () => { // wait for parent Ajax call to finish await waitFor(() => expect(test.element).not.toHaveAttribute('busy')); // sanity check - expect(test.element).toHaveTextContent('Using Original child: no') + expect(test.element).toHaveTextContent('Using Original child: no'); // after the parent re-renders, the child should already have received its new fingerprint expect(childComponent.fingerprint).toEqual('updated fingerprint'); @@ -286,7 +307,7 @@ describe('Component parent -> child initialization and rendering tests', () => { // child component re-rendered and there are a few important things here // 1) the toUppercase prop was changed by the parent and that change remains // 2) The " Weaver" change to the "firstName" data was kept, not "run over" - expect(childComponent.element).toHaveTextContent('Full Name: RYAN WEAVER') + expect(childComponent.element).toHaveTextContent('Full Name: RYAN WEAVER'); }); it('child controller changes its component if child id changes', async () => { @@ -304,29 +325,31 @@ describe('Component parent -> child initialization and rendering tests', () => { `; - const test = await createTest({useOriginalChild: true}, (data: any) => ` + const test = await createTest( + { useOriginalChild: true }, + (data: any) => `
Parent Component - ${data.useOriginalChild ? originalChildTemplate({name: 'original'}) : reRenderedChildTemplate({name: 'new'})} + ${data.useOriginalChild ? originalChildTemplate({ name: 'original' }) : reRenderedChildTemplate({ name: 'new' })}
- `); + ` + ); const originalChildElement = getByTestId(test.element, 'child-component'); // Re-render the parent - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // trigger the re-rendered child to be used - data.useOriginalChild = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // trigger the re-rendered child to be used + data.useOriginalChild = false; + }); test.component.render(); // wait for parent Ajax call to start/finish await waitFor(() => expect(test.element).toHaveAttribute('busy')); await waitFor(() => expect(test.element).not.toHaveAttribute('busy')); // no child Ajax call made: we simply use the new child's content - expect(test.element).toHaveTextContent('New Child') - expect(test.element).not.toHaveTextContent('Original Child') + expect(test.element).toHaveTextContent('New Child'); + expect(test.element).not.toHaveTextContent('Original Child'); expect(findChildren(test.component).length).toEqual(1); const newChildElement = getByTestId(test.element, 'child-component'); @@ -351,19 +374,23 @@ describe('Component parent -> child initialization and rendering tests', () => { > `; - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
${childTemplate({ number: 1, value: 'Original value for child 1' })}
Parent Component
${childTemplate({ number: 2, value: 'Original value for child 2' })}
- `); + ` + ); // Re-render the parent test.expectsAjaxCall() // return the template in a different order // and render children with an updated value prop - .willReturn((data: any) => ` + .willReturn( + (data: any) => `
${emptyChildTemplate({ number: 2, value: 'New value for child 2' })} @@ -375,7 +402,8 @@ describe('Component parent -> child initialization and rendering tests', () => {
- `); + ` + ); const childComponent1 = getComponent(getByTestId(test.element, 'child-component-1')); const childTest1 = createTestForExistingComponent(childComponent1); @@ -383,21 +411,22 @@ describe('Component parent -> child initialization and rendering tests', () => { const childTest2 = createTestForExistingComponent(childComponent2); // Expect both children to re-render - childTest1.expectsAjaxCall() + childTest1 + .expectsAjaxCall() // new props are sent, but that doesn't count as updated data // we verify the new props are used below by checking the HTML - .expectUpdatedData({ }) + .expectUpdatedData({}) .expectUpdatedPropsFromParent({ number: 1, value: 'New value for child 1' }) .willReturn(childTemplate); - childTest2.expectsAjaxCall() - .expectUpdatedData({ }) + childTest2 + .expectsAjaxCall() + .expectUpdatedData({}) .expectUpdatedPropsFromParent({ number: 2, value: 'New value for child 2' }) .willReturn(childTemplate); // trigger the parent render, which will trigger the children to re-render await test.component.render(); - // wait for child to start and stop loading await waitFor(() => expect(getByTestId(test.element, 'child-component-1')).not.toHaveAttribute('busy')); await waitFor(() => expect(getByTestId(test.element, 'child-component-2')).not.toHaveAttribute('busy')); diff --git a/src/LiveComponent/assets/test/controller/dispatch-event.test.ts b/src/LiveComponent/assets/test/controller/dispatch-event.test.ts index ae5ad5501f1..e20283e91c1 100644 --- a/src/LiveComponent/assets/test/controller/dispatch-event.test.ts +++ b/src/LiveComponent/assets/test/controller/dispatch-event.test.ts @@ -7,19 +7,22 @@ * file that was distributed with this source code. */ -import {createTest, initComponent, shutdownTests} from '../tools'; +import { createTest, initComponent, shutdownTests } from '../tools'; describe('LiveController Event Dispatching Tests', () => { afterEach(() => { - shutdownTests() + shutdownTests(); }); it('dispatches events sent from an AJAX request', async () => { - const test = await createTest({ }, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
Simple Component!
- `); + ` + ); let eventCalled = false; test.element.addEventListener('fooEvent', (event: any) => { @@ -27,13 +30,11 @@ describe('LiveController Event Dispatching Tests', () => { expect(event.detail).toEqual({ foo: 'bar' }); }); - test.expectsAjaxCall() - .willReturn(() => ` -
Simple Component!
- `); + test.expectsAjaxCall().willReturn( + () => ` +
Simple Component!
+ ` + ); await test.component.render(); expect(eventCalled).toBe(true); diff --git a/src/LiveComponent/assets/test/controller/emit.test.ts b/src/LiveComponent/assets/test/controller/emit.test.ts index 87c72eb7302..f9727671d76 100644 --- a/src/LiveComponent/assets/test/controller/emit.test.ts +++ b/src/LiveComponent/assets/test/controller/emit.test.ts @@ -7,19 +7,21 @@ * file that was distributed with this source code. */ -import {createTest, initComponent, shutdownTests} from '../tools'; +import { createTest, initComponent, shutdownTests } from '../tools'; import { getByText, waitFor } from '@testing-library/dom'; describe('LiveController Emit Tests', () => { afterEach(() => { - shutdownTests() + shutdownTests(); }); it('emits event using emit()', async () => { - const test = await createTest({ renderCount: 0 }, (data: any) => ` + const test = await createTest( + { renderCount: 0 }, + (data: any) => `
Render Count: ${data.renderCount}
- `); + ` + ); test.expectsAjaxCall() .expectActionCalled('fooAction') .serverWillChangeProps((data) => { data.renderCount = 1; - }) + }); getByText(test.element, 'Emit Simple').click(); await waitFor(() => expect(test.element).toHaveTextContent('Render Count: 1')); @@ -52,7 +55,7 @@ describe('LiveController Emit Tests', () => { .expectActionCalled('fooAction') .serverWillChangeProps((data) => { data.renderCount = 2; - }) + }); getByText(test.element, 'Emit Named Matching').click(); await waitFor(() => expect(test.element).toHaveTextContent('Render Count: 2')); @@ -64,14 +67,14 @@ describe('LiveController Emit Tests', () => { }); it('emits event sent back after Ajax call', async () => { - const test = await createTest({ renderCount: 0 }, (data: any) => ` + const test = await createTest( + { renderCount: 0 }, + (data: any) => `
Render Count: ${data.renderCount}
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data) => { - data.renderCount = 1; - }) + test.expectsAjaxCall().serverWillChangeProps((data) => { + data.renderCount = 1; + }); test.component.render(); diff --git a/src/LiveComponent/assets/test/controller/error.test.ts b/src/LiveComponent/assets/test/controller/error.test.ts index 8cca5885fe2..883cc77ed0c 100644 --- a/src/LiveComponent/assets/test/controller/error.test.ts +++ b/src/LiveComponent/assets/test/controller/error.test.ts @@ -11,28 +11,34 @@ import { createTest, initComponent, shutdownTests } from '../tools'; import { getByText, waitFor } from '@testing-library/dom'; import type BackendResponse from '../../src/Backend/BackendResponse'; -const getErrorElement = (): Element|null => { +const getErrorElement = (): Element | null => { return document.getElementById('live-component-error'); }; describe('LiveController Error Handling', () => { afterEach(() => { shutdownTests(); - }) + }); it('displays an error modal on 500 errors', async () => { - const test = await createTest({ counter: 4 }, (data: any) => ` + const test = await createTest( + { counter: 4 }, + (data: any) => `
Current count: ${data.counter}
- `); + ` + ); test.expectsAjaxCall() - .serverWillReturnCustomResponse(500, ` + .serverWillReturnCustomResponse( + 500, + ` Error!

An error occurred

- `) + ` + ) .expectActionCalled('save'); getByText(test.element, 'Save').click(); @@ -47,27 +53,32 @@ describe('LiveController Error Handling', () => { expect(errorContainer.querySelector('iframe')).not.toBeNull(); // make sure future requests can still be sent - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.counter = 10; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.counter = 10; + }); getByText(test.element, 'Render').click(); await waitFor(() => expect(test.element).toHaveTextContent('Current count: 10')); }); it('displays a modal on any non-component response', async () => { - const test = await createTest({ }, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
Original component text
- `); + ` + ); test.expectsAjaxCall() - .serverWillReturnCustomResponse(200, ` + .serverWillReturnCustomResponse( + 200, + ` Hi!

I'm a whole page, not a component!

- `) + ` + ) .expectActionCalled('save'); getByText(test.element, 'Save').click(); @@ -78,16 +89,22 @@ describe('LiveController Error Handling', () => { }); it('triggers response:error hook', async () => { - const test = await createTest({ }, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
component text
- `); + ` + ); test.expectsAjaxCall() - .serverWillReturnCustomResponse(200, ` + .serverWillReturnCustomResponse( + 200, + ` Hi!

I'm a whole page, not a component!

- `) + ` + ) .expectActionCalled('save'); let isHookCalled = false; diff --git a/src/LiveComponent/assets/test/controller/loading.test.ts b/src/LiveComponent/assets/test/controller/loading.test.ts index dd724e841f9..72a078547a5 100644 --- a/src/LiveComponent/assets/test/controller/loading.test.ts +++ b/src/LiveComponent/assets/test/controller/loading.test.ts @@ -7,22 +7,25 @@ * file that was distributed with this source code. */ -import {createTest, initComponent, shutdownTests} from '../tools'; -import {getByTestId, getByText, waitFor} from '@testing-library/dom'; +import { createTest, initComponent, shutdownTests } from '../tools'; +import { getByTestId, getByText, waitFor } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; describe('LiveController data-loading Tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('executes basic loading functionality on an element', async () => { - const test = await createTest({food: 'pizza'}, (data: any) => ` + const test = await createTest( + { food: 'pizza' }, + (data: any) => `
I like: ${data.food} Loading...
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data: any) => { @@ -46,12 +49,15 @@ describe('LiveController data-loading Tests', () => { }); it('executes basic loading functionality on root element', async () => { - const test = await createTest({food: 'pizza'}, (data: any) => ` + const test = await createTest( + { food: 'pizza' }, + (data: any) => `
I like: ${data.food}
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data: any) => { @@ -75,7 +81,9 @@ describe('LiveController data-loading Tests', () => { }); it('takes into account the "action" modifier', async () => { - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
Loading... @@ -83,7 +91,8 @@ describe('LiveController data-loading Tests', () => {
- `); + ` + ); test.expectsAjaxCall() // delay so we can check loading @@ -119,7 +128,9 @@ describe('LiveController data-loading Tests', () => { }); it('takes into account the "model" modifier', async () => { - const test = await createTest({ comments: '', user: { email: '' }}, (data: any) => ` + const test = await createTest( + { comments: '', user: { email: '' } }, + (data: any) => `
Comments change loading... @@ -127,14 +138,15 @@ describe('LiveController data-loading Tests', () => { Checking if email is taken...
- `); + ` + ); test.expectsAjaxCall() .expectUpdatedData({ comments: 'Changing the comments!' }) // delay so we can check loading .delayResponse(50); - userEvent.type(test.queryByDataModel('comments'), 'Changing the comments!') + userEvent.type(test.queryByDataModel('comments'), 'Changing the comments!'); // it should not be loading yet due to debouncing expect(getByTestId(test.element, 'comments-loading')).not.toBeVisible(); // wait for ajax call to start @@ -170,14 +182,17 @@ describe('LiveController data-loading Tests', () => { }); it('can handle multiple actions on the same request', async () => { - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
Loading...
- `); + ` + ); // 1 ajax request with both actions test.expectsAjaxCall() @@ -197,13 +212,16 @@ describe('LiveController data-loading Tests', () => { }); it('does not trigger loading if request finishes first', async () => { - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
Loading...
- `); + ` + ); test.expectsAjaxCall() .expectActionCalled('save') @@ -217,12 +235,12 @@ describe('LiveController data-loading Tests', () => { // request took 30ms, action loading is delayed for 50 // wait 30 more (30+30=60) and verify the element did not switch into loading - await (new Promise(resolve => setTimeout(resolve, 30))); + await new Promise((resolve) => setTimeout(resolve, 30)); expect(getByTestId(test.element, 'loading-element')).not.toBeVisible(); }); it('does not trigger loading inside component children', async () => { - const childTemplate = (data: any) => ` + const childTemplate = (data: any) => `
{
`; - const test = await createTest({renderChild: true} , (data: any) => ` -
+ const test = await createTest( + { renderChild: true }, + (data: any) => ` +
Loading... Loading... - ${childTemplate({renderChild: data.renderChild})} + ${childTemplate({ renderChild: data.renderChild })}
- `); + ` + ); test.expectsAjaxCall() // delay so we can check loading @@ -266,7 +287,7 @@ describe('LiveController data-loading Tests', () => { expect(getByTestId(test.element, 'child-loading-element-hiding')).toBeVisible(); // wait for loading to finish - await (new Promise(resolve => setTimeout(resolve, 30))); + await new Promise((resolve) => setTimeout(resolve, 30)); // Parent: back to original state expect(getByTestId(test.element, 'parent-loading-element-showing')).not.toBeVisible(); diff --git a/src/LiveComponent/assets/test/controller/model.test.ts b/src/LiveComponent/assets/test/controller/model.test.ts index 3e6012a3dc6..e0d92ce061f 100644 --- a/src/LiveComponent/assets/test/controller/model.test.ts +++ b/src/LiveComponent/assets/test/controller/model.test.ts @@ -14,10 +14,12 @@ import userEvent from '@testing-library/user-event'; describe('LiveController data-model Tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('sends data and re-renders correctly when data-model element is changed', async () => { - const test = await createTest({ name: 'Ryan' }, (data: any) => ` + const test = await createTest( + { name: 'Ryan' }, + (data: any) => `
{ Name is: ${data.name}
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ name: 'Ryan Weaver' }); + test.expectsAjaxCall().expectUpdatedData({ name: 'Ryan Weaver' }); await userEvent.type(test.queryByDataModel('name'), ' Weaver', { // this tests the debounce: characters have a 10ms delay // in between, but the debouncing prevents multiple calls - delay: 10 + delay: 10, }); await waitFor(() => expect(test.element).toHaveTextContent('Name is: Ryan Weaver')); - expect(test.component.valueStore.getOriginalProps()).toEqual({name: 'Ryan Weaver'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ name: 'Ryan Weaver' }); // assert the input is still focused after rendering expect(document.activeElement).toBeInstanceOf(HTMLElement); @@ -46,7 +48,9 @@ describe('LiveController data-model Tests', () => { }); it('updates the data without re-rendering if "norender" is used', async () => { - const test = await createTest({ name: 'Ryan' }, (data: any) => ` + const test = await createTest( + { name: 'Ryan' }, + (data: any) => `
{ Name is: ${data.name}
- `); + ` + ); await userEvent.type(test.queryByDataModel('name'), ' Weaver', { // debounce is only 1, so this "would" send MANY Ajax requests // if "norender" were NOT used - delay: 10 + delay: 10, }); // component never re-rendered @@ -70,7 +75,9 @@ describe('LiveController data-model Tests', () => { }); it('waits to update data and rerender until change event with on(change)', async () => { - const test = await createTest({ name: 'Ryan' }, (data: any) => ` + const test = await createTest( + { name: 'Ryan' }, + (data: any) => `
{ Name is: ${data.name}
- `); + ` + ); await userEvent.type(test.queryByDataModel('name'), ' Weaver', { // debounce is only 1, so this "would" send MANY Ajax requests // if on(change) were NOT used (each character only triggers an "input" event) - delay: 10 + delay: 10, }); // component has not *yet* re-rendered expect(test.element).toHaveTextContent('Name is: Ryan'); // the read-only props have not *yet* been updated - expect(test.component.valueStore.getOriginalProps()).toEqual({name: 'Ryan'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ name: 'Ryan' }); // NOW we expect the render - test.expectsAjaxCall() - .expectUpdatedData({ name: 'Ryan Weaver' }); + test.expectsAjaxCall().expectUpdatedData({ name: 'Ryan Weaver' }); // this will cause the input to "blur" and trigger the change event userEvent.click(getByText(test.element, 'Do nothing')); await waitFor(() => expect(test.element).toHaveTextContent('Name is: Ryan Weaver')); - expect(test.component.valueStore.getOriginalProps()).toEqual({name: 'Ryan Weaver'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ name: 'Ryan Weaver' }); }); it('renders correctly with data-value and live#update on a non-input', async () => { - const test = await createTest({ name: 'Ryan' }, (data: any) => ` + const test = await createTest( + { name: 'Ryan' }, + (data: any) => ` - `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ name: 'Jan' }); + test.expectsAjaxCall().expectUpdatedData({ name: 'Jan' }); userEvent.click(getByText(test.element, 'Change name to Jan')); await waitFor(() => expect(test.element).toHaveTextContent('Name is: Jan')); - expect(test.component.valueStore.getOriginalProps()).toEqual({name: 'Jan'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ name: 'Jan' }); - test.expectsAjaxCall() - .expectUpdatedData({ name: 'Dan' }); + test.expectsAjaxCall().expectUpdatedData({ name: 'Dan' }); userEvent.click(getByText(test.element, 'Change name to Dan')); await waitFor(() => expect(test.element).toHaveTextContent('Name is: Dan')); - expect(test.component.valueStore.getOriginalProps()).toEqual({name: 'Dan'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ name: 'Dan' }); }); it('falls back to using the name attribute when no data-model is present and
is ancestor', async () => { - const test = await createTest({ color: '' }, (data: any) => ` + const test = await createTest( + { color: '' }, + (data: any) => `
{ Favorite color: ${data.color}
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ color: 'orange' }); + test.expectsAjaxCall().expectUpdatedData({ color: 'orange' }); await userEvent.type(test.queryByNameAttribute('color'), 'orange'); @@ -164,7 +174,9 @@ describe('LiveController data-model Tests', () => { }); it('uses data-model when both name and data-model is present', async () => { - const test = await createTest({ name: '', firstName: '' }, (data: any) => ` + const test = await createTest( + { name: '', firstName: '' }, + (data: any) => `
{ First name: ${data.firstName}
- `); + ` + ); test.expectsAjaxCall() // firstName is the model that is matched and updated @@ -188,7 +201,9 @@ describe('LiveController data-model Tests', () => { }); it('uses data-value when both value and data-value is present', async () => { - const test = await createTest({ sport: '' }, (data: any) => ` + const test = await createTest( + { sport: '' }, + (data: any) => `
{ Sport: ${data.sport}
- `); + ` + ); test.expectsAjaxCall() // "cross country" takes precedence over real value @@ -209,7 +225,9 @@ describe('LiveController data-model Tests', () => { }); it('standardizes user[name] style models into user.name', async () => { - const test = await createTest({ user: { name: 'Ryan' } }, (data: any) => ` + const test = await createTest( + { user: { name: 'Ryan' } }, + (data: any) => `
{ Name: ${data.user.name}
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ 'user.name': 'Ryan Weaver' }); + test.expectsAjaxCall().expectUpdatedData({ 'user.name': 'Ryan Weaver' }); await userEvent.type(test.queryByDataModel('user[name]'), ' Weaver'); @@ -239,10 +257,10 @@ describe('LiveController data-model Tests', () => { Name: ${props['user.name']}
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ 'user.name': 'Ryan Weaver' }); + test.expectsAjaxCall().expectUpdatedData({ 'user.name': 'Ryan Weaver' }); await userEvent.type(test.queryByDataModel('user.name'), ' Weaver'); @@ -251,10 +269,14 @@ describe('LiveController data-model Tests', () => { }); it('sends correct data for checkbox fields', async () => { - const test = await createTest({ form: { - check1: false, - check2: false - } }, (data: any) => ` + const test = await createTest( + { + form: { + check1: false, + check2: false, + }, + }, + (data: any) => `
- Checkbox 2 is ${data.form.check2 ? 'checked' : 'unchecked' } + Checkbox 2 is ${data.form.check2 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.check1': '1', 'form.check2': '1' }); + test.expectsAjaxCall().expectUpdatedData({ 'form.check1': '1', 'form.check2': '1' }); await userEvent.click(check2Element); await userEvent.click(check1Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 2 is checked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check1: '1', check2: '1'}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { check1: '1', check2: '1' } }); }); it('sends correct data for initially checked checkbox fields', async () => { - const test = await createTest({ form: { - check1: '1', - check2: false - } }, (data: any) => ` + const test = await createTest( + { + form: { + check1: '1', + check2: false, + }, + }, + (data: any) => `
- Checkbox 1 is ${data.form.check1 ? 'checked' : 'unchecked' } + Checkbox 1 is ${data.form.check1 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.check1': null, 'form.check2': '1' }); + test.expectsAjaxCall().expectUpdatedData({ 'form.check1': null, 'form.check2': '1' }); await userEvent.click(check2Element); await userEvent.click(check1Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 1 is unchecked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check1: null, check2: '1'}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { check1: null, check2: '1' } }); }); it('sends correct data for array valued checkbox fields', async () => { - const test = await createTest({ form: { check: [] } }, (data: any) => ` + const test = await createTest( + { form: { check: [] } }, + (data: any) => `
- Checkbox 2 is ${data.form.check.indexOf('bar') > -1 ? 'checked' : 'unchecked' } + Checkbox 2 is ${data.form.check.indexOf('bar') > -1 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.check': ['foo', 'bar'] }); + test.expectsAjaxCall().expectUpdatedData({ 'form.check': ['foo', 'bar'] }); await userEvent.click(check1Element); await userEvent.click(check2Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 2 is checked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check: ['foo', 'bar']}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { check: ['foo', 'bar'] } }); }); it('sends correct data for array valued checkbox fields with non-form object', async () => { - const test = await createTest({ check: [] }, (data: any) => ` + const test = await createTest( + { check: [] }, + (data: any) => `
- Checkbox 2 is ${data.check.indexOf('bar') > -1 ? 'checked' : 'unchecked' } + Checkbox 2 is ${data.check.indexOf('bar') > -1 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ check: ['foo', 'bar'] }); + test.expectsAjaxCall().expectUpdatedData({ check: ['foo', 'bar'] }); await userEvent.click(check1Element); await userEvent.click(check2Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 2 is checked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({check: ['foo', 'bar']}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ check: ['foo', 'bar'] }); }); it('sends correct data for array valued checkbox fields with initial data', async () => { - const test = await createTest({ form: { check: ['foo']} }, (data: any) => ` + const test = await createTest( + { form: { check: ['foo'] } }, + (data: any) => `
- Checkbox 1 is ${data.form.check.indexOf('foo') > -1 ? 'checked' : 'unchecked' } + Checkbox 1 is ${data.form.check.indexOf('foo') > -1 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.check': ['bar'] }); + test.expectsAjaxCall().expectUpdatedData({ 'form.check': ['bar'] }); await userEvent.click(check2Element); await userEvent.click(check1Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 1 is unchecked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {check: ['bar']}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { check: ['bar'] } }); }); it('sends correct data for array valued checkbox fields with non-form object and with initial data', async () => { - const test = await createTest({ check: ['foo'] }, (data: any) => ` + const test = await createTest( + { check: ['foo'] }, + (data: any) => `
- Checkbox 1 is ${data.check.indexOf('foo') > -1 ? 'checked' : 'unchecked' } + Checkbox 1 is ${data.check.indexOf('foo') > -1 ? 'checked' : 'unchecked'}
- `); + ` + ); const check1Element = getByLabelText(test.element, 'Checkbox 1:'); const check2Element = getByLabelText(test.element, 'Checkbox 2:'); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ check: ['bar'] }); + test.expectsAjaxCall().expectUpdatedData({ check: ['bar'] }); await userEvent.click(check1Element); await userEvent.click(check2Element); await waitFor(() => expect(test.element).toHaveTextContent('Checkbox 1 is unchecked')); - expect(test.component.valueStore.getOriginalProps()).toEqual({check: ['bar']}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ check: ['bar'] }); }); it('sends correct data for select multiple field', async () => { - const test = await createTest({ form: { select: []} }, (data: any) => ` + const test = await createTest( + { form: { select: [] } }, + (data: any) => `
- Option 2 is ${data.form.select?.indexOf('bar') > -1 ? 'selected' : 'unselected' } + Option 2 is ${data.form.select?.indexOf('bar') > -1 ? 'selected' : 'unselected'}
- `); + ` + ); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.select': ['foo', 'bar'] }); + test.expectsAjaxCall().expectUpdatedData({ 'form.select': ['foo', 'bar'] }); const selectElement = getByLabelText(test.element, 'Select:'); await userEvent.selectOptions(selectElement, 'foo'); @@ -472,11 +508,13 @@ describe('LiveController data-model Tests', () => { await waitFor(() => expect(test.element).toHaveTextContent('Select: foo bar Option 2 is selected')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {select: ['foo', 'bar']}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { select: ['foo', 'bar'] } }); }); it('sends correct data for select multiple field with initial data', async () => { - const test = await createTest({ form: { select: ['foo']} }, (data: any) => ` + const test = await createTest( + { form: { select: ['foo'] } }, + (data: any) => `
- Option 2 is ${data.form.select?.indexOf('bar') > -1 ? 'selected' : 'unselected' } + Option 2 is ${data.form.select?.indexOf('bar') > -1 ? 'selected' : 'unselected'}
- `); + ` + ); // only 1 Ajax call will be made thanks to debouncing - test.expectsAjaxCall() - .expectUpdatedData({ 'form.select': ['bar'] }); + test.expectsAjaxCall().expectUpdatedData({ 'form.select': ['bar'] }); const selectElement = getByLabelText(test.element, 'Select:'); await userEvent.selectOptions(selectElement, 'bar'); @@ -502,17 +540,18 @@ describe('LiveController data-model Tests', () => { await waitFor(() => expect(test.element).toHaveTextContent('Select: foo bar Option 2 is selected')); - test.expectsAjaxCall() - .expectUpdatedData({ 'form.select': [] }); + test.expectsAjaxCall().expectUpdatedData({ 'form.select': [] }); await userEvent.deselectOptions(selectElement, 'bar'); await waitFor(() => expect(test.element).toHaveTextContent('Select: foo bar Option 2 is unselected')); - expect(test.component.valueStore.getOriginalProps()).toEqual({form: {select: []}}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ form: { select: [] } }); }); it('tracks which fields should be validated and sends, without forgetting previous fields', async () => { // start with one field in validatedFields - const test = await createTest({ treat: '', validatedFields: ['otherField'] }, (data: any) => ` + const test = await createTest( + { treat: '', validatedFields: ['otherField'] }, + (data: any) => `
{ Treat: ${data.treat}
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ - treat: 'ice cream', - validatedFields: ['otherField', 'treat'] - }); + test.expectsAjaxCall().expectUpdatedData({ + treat: 'ice cream', + validatedFields: ['otherField', 'treat'], + }); await userEvent.type(test.queryByDataModel('treat'), 'ice cream'); @@ -534,7 +573,9 @@ describe('LiveController data-model Tests', () => { }); it('data changed on server should be noticed by controller and used in dataValue', async () => { - const test = await createTest({ pizzaTopping: '' }, (data: any) => ` + const test = await createTest( + { pizzaTopping: '' }, + (data: any) => `
{ Mmmm ${data.pizzaTopping} pizza
- `); + ` + ); test.expectsAjaxCall() .expectUpdatedData({ pizzaTopping: 'mushroom' }) @@ -559,7 +601,9 @@ describe('LiveController data-model Tests', () => { }); it('sends a render request without debounce for change events', async () => { - const test = await createTest({ firstName: '', lastName: '' }, (data: any) => ` + const test = await createTest( + { firstName: '', lastName: '' }, + (data: any) => `
@@ -569,13 +613,12 @@ describe('LiveController data-model Tests', () => { First Name: ${data.firstName} Last Name: ${data.lastName}
- `); + ` + ); // TWO requests because debouncing doesn't prevent the 2nd - test.expectsAjaxCall() - .expectUpdatedData({ firstName: 'Ryan' }); - test.expectsAjaxCall() - .expectUpdatedData({ lastName: 'Weaver' }); + test.expectsAjaxCall().expectUpdatedData({ firstName: 'Ryan' }); + test.expectsAjaxCall().expectUpdatedData({ lastName: 'Weaver' }); await userEvent.type(test.queryByDataModel('firstName'), 'Ryan'); await userEvent.type(test.queryByDataModel('lastName'), 'Weaver', { @@ -593,7 +636,9 @@ describe('LiveController data-model Tests', () => { }); it('notices the "real" value of a select without an empty value', async () => { - const test = await createTest({ food: '' }, (data: any) => ` + const test = await createTest( + { food: '' }, + (data: any) => `
@@ -670,7 +719,8 @@ describe('LiveController data-model Tests', () => { Comment: ${data.comment}
- `); + ` + ); const foodSelect = test.queryByDataModel('food'); if (!(foodSelect instanceof HTMLSelectElement)) { @@ -703,7 +753,9 @@ describe('LiveController data-model Tests', () => { }); it('does not try to set the value of inputs inside a child component', async () => { - const test = await createTest({ comment: 'cookie', childComment: 'mmmm', skipChild: false }, (data: any) => ` + const test = await createTest( + { comment: 'cookie', childComment: 'mmmm', skipChild: false }, + (data: any) => `
@@ -715,7 +767,8 @@ describe('LiveController data-model Tests', () => {
- `); + ` + ); const commentField = test.element.querySelector('#parent-comment'); if (!(commentField instanceof HTMLTextAreaElement)) { @@ -745,11 +798,13 @@ describe('LiveController data-model Tests', () => { }); it('keeps the unsynced value of an input on re-render, but accepts other changes to the field', async () => { - const test = await createTest({ - comment: 'Live components', - unmappedTextareaValue: 'no data-model', - fieldClass: 'initial-class' - }, (data: any) => ` + const test = await createTest( + { + comment: 'Live components', + unmappedTextareaValue: 'no data-model', + fieldClass: 'initial-class', + }, + (data: any) => `
@@ -759,7 +814,8 @@ describe('LiveController data-model Tests', () => {
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data) => { @@ -778,7 +834,7 @@ describe('LiveController data-model Tests', () => { commentField.dispatchEvent(new Event('input', { bubbles: true })); // also type into the unmapped field - but no worry about the model sync'ing this time - userEvent.type(getByTestId(test.element, 'unmappedTextarea'), ' here!') + userEvent.type(getByTestId(test.element, 'unmappedTextarea'), ' here!'); await waitFor(() => expect(test.element).toHaveTextContent('FieldClass: changed-class')); // re-find in case the element itself has changed by morphdom @@ -802,9 +858,11 @@ describe('LiveController data-model Tests', () => { }); it('keeps the unsynced value of a model field mapped via a form', async () => { - const test = await createTest({ - comment: 'Live components', - }, (data: any) => ` + const test = await createTest( + { + comment: 'Live components', + }, + (data: any) => `
@@ -812,7 +870,8 @@ describe('LiveController data-model Tests', () => {
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data) => { @@ -850,7 +909,9 @@ describe('LiveController data-model Tests', () => { }); it('allows model fields to be manually set as long as change event is dispatched', async () => { - const test = await createTest({ food: '' }, (data: any) => ` + const test = await createTest( + { food: '' }, + (data: any) => `
{ Name: ${data.name} Render count: ${data.renderCount}
- `); + ` + ); // First request, from typing (debouncing is set to 1ms) test.expectsAjaxCall() - .expectUpdatedData({ name: 'Ryan Weaver'}) + .expectUpdatedData({ name: 'Ryan Weaver' }) .serverWillChangeProps((data: any) => { data.renderCount = 1; }) @@ -190,11 +201,10 @@ describe('LiveController polling Tests', () => { setTimeout(() => { // first poll, should happen after 50 ms, but needs to wait the full 100 - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.renderCount = 2; - }); - }, 75) + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.renderCount = 2; + }); + }, 75); await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1')); await waitFor(() => expect(test.element).toHaveTextContent('Render count: 2')); diff --git a/src/LiveComponent/assets/test/controller/query-binding.test.ts b/src/LiveComponent/assets/test/controller/query-binding.test.ts index c8db7504532..a61f1c0e938 100644 --- a/src/LiveComponent/assets/test/controller/query-binding.test.ts +++ b/src/LiveComponent/assets/test/controller/query-binding.test.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import {createTest, initComponent, shutdownTests, setCurrentSearch, expectCurrentSearch} from '../tools'; +import { createTest, initComponent, shutdownTests, setCurrentSearch, expectCurrentSearch } from '../tools'; import { getByText, waitFor } from '@testing-library/dom'; describe('LiveController query string binding', () => { @@ -16,40 +16,47 @@ describe('LiveController query string binding', () => { setCurrentSearch(''); }); - it('doesn\'t initialize URL if props are not defined', async () => { - await createTest({ prop: ''}, (data: any) => ` -
- `) + it("doesn't initialize URL if props are not defined", async () => { + await createTest( + { prop: '' }, + (data: any) => ` +
+ ` + ); expectCurrentSearch().toEqual(''); - }) + }); - it('doesn\'t initialize URL with defined props values', async () => { - await createTest({ prop: 'foo'}, (data: any) => ` -
- `) + it("doesn't initialize URL with defined props values", async () => { + await createTest( + { prop: 'foo' }, + (data: any) => ` +
+ ` + ); expectCurrentSearch().toEqual(''); }); it('updates basic props in the URL', async () => { - const test = await createTest({ prop1: '', prop2: null}, (data: any) => ` -
- `) + const test = await createTest( + { prop1: '', prop2: null }, + (data: any) => ` +
+ ` + ); // String // Set value - test.expectsAjaxCall() - .expectUpdatedData({prop1: 'foo'}); + test.expectsAjaxCall().expectUpdatedData({ prop1: 'foo' }); await test.component.set('prop1', 'foo', true); expectCurrentSearch().toEqual('?prop1=foo&prop2='); // Remove value - test.expectsAjaxCall() - .expectUpdatedData({prop1: ''}); + test.expectsAjaxCall().expectUpdatedData({ prop1: '' }); await test.component.set('prop1', '', true); @@ -58,16 +65,14 @@ describe('LiveController query string binding', () => { // Number // Set value - test.expectsAjaxCall() - .expectUpdatedData({prop2: 42}); + test.expectsAjaxCall().expectUpdatedData({ prop2: 42 }); await test.component.set('prop2', 42, true); expectCurrentSearch().toEqual('?prop1=&prop2=42'); // Remove value - test.expectsAjaxCall() - .expectUpdatedData({prop2: null}); + test.expectsAjaxCall().expectUpdatedData({ prop2: null }); await test.component.set('prop2', null, true); @@ -75,29 +80,29 @@ describe('LiveController query string binding', () => { }); it('updates array props in the URL', async () => { - const test = await createTest({ prop: []}, (data: any) => ` -
- `) + const test = await createTest( + { prop: [] }, + (data: any) => ` +
+ ` + ); // Set value - test.expectsAjaxCall() - .expectUpdatedData({prop: ['foo', 'bar']}); + test.expectsAjaxCall().expectUpdatedData({ prop: ['foo', 'bar'] }); await test.component.set('prop', ['foo', 'bar'], true); expectCurrentSearch().toEqual('?prop[0]=foo&prop[1]=bar'); // Remove one value - test.expectsAjaxCall() - .expectUpdatedData({prop: ['foo']}); + test.expectsAjaxCall().expectUpdatedData({ prop: ['foo'] }); await test.component.set('prop', ['foo'], true); expectCurrentSearch().toEqual('?prop[0]=foo'); // Remove all remaining values - test.expectsAjaxCall() - .expectUpdatedData({prop: []}); + test.expectsAjaxCall().expectUpdatedData({ prop: [] }); await test.component.set('prop', [], true); @@ -105,37 +110,36 @@ describe('LiveController query string binding', () => { }); it('updates objects in the URL', async () => { - const test = await createTest({ prop: { foo: null, bar: null, baz: null}}, (data: any) => ` -
- `) + const test = await createTest( + { prop: { foo: null, bar: null, baz: null } }, + (data: any) => ` +
+ ` + ); // Set single nested prop - test.expectsAjaxCall() - .expectUpdatedData({'prop.foo': 'dummy' }); + test.expectsAjaxCall().expectUpdatedData({ 'prop.foo': 'dummy' }); await test.component.set('prop.foo', 'dummy', true); expectCurrentSearch().toEqual('?prop[foo]=dummy'); // Set multiple values - test.expectsAjaxCall() - .expectUpdatedData({prop: { foo: 'other', bar: 42 } }); + test.expectsAjaxCall().expectUpdatedData({ prop: { foo: 'other', bar: 42 } }); await test.component.set('prop', { foo: 'other', bar: 42 }, true); expectCurrentSearch().toEqual('?prop[foo]=other&prop[bar]=42'); // Remove one value - test.expectsAjaxCall() - .expectUpdatedData({prop: { foo: 'other', bar: null } }); + test.expectsAjaxCall().expectUpdatedData({ prop: { foo: 'other', bar: null } }); await test.component.set('prop', { foo: 'other', bar: null }, true); expectCurrentSearch().toEqual('?prop[foo]=other'); // Remove all values - test.expectsAjaxCall() - .expectUpdatedData({prop: { foo: null, bar: null } }); + test.expectsAjaxCall().expectUpdatedData({ prop: { foo: null, bar: null } }); await test.component.set('prop', { foo: null, bar: null }, true); @@ -143,12 +147,15 @@ describe('LiveController query string binding', () => { }); it('updates the URL with props changed by the server', async () => { - const test = await createTest({ prop: ''}, (data: any) => ` -
+ const test = await createTest( + { prop: '' }, + (data: any) => ` +
Prop: ${data.prop}
- `); + ` + ); test.expectsAjaxCall() .expectActionCalled('changeProp') @@ -164,24 +171,25 @@ describe('LiveController query string binding', () => { }); it('uses custom name instead of prop name in the URL', async () => { - const test = await createTest({ prop1: ''}, (data: any) => ` -
- `) + const test = await createTest( + { prop1: '' }, + (data: any) => ` +
+ ` + ); // Set value - test.expectsAjaxCall() - .expectUpdatedData({prop1: 'foo'}); + test.expectsAjaxCall().expectUpdatedData({ prop1: 'foo' }); await test.component.set('prop1', 'foo', true); expectCurrentSearch().toEqual('?alias1=foo'); // Remove value - test.expectsAjaxCall() - .expectUpdatedData({prop1: ''}); + test.expectsAjaxCall().expectUpdatedData({ prop1: '' }); await test.component.set('prop1', '', true); expectCurrentSearch().toEqual('?alias1='); }); -}) +}); diff --git a/src/LiveComponent/assets/test/controller/render-with-external-changes.test.ts b/src/LiveComponent/assets/test/controller/render-with-external-changes.test.ts index 7081819fd0c..8eb3e0348a8 100644 --- a/src/LiveComponent/assets/test/controller/render-with-external-changes.test.ts +++ b/src/LiveComponent/assets/test/controller/render-with-external-changes.test.ts @@ -14,10 +14,12 @@ import { htmlToElement } from '../../src/dom_utils'; describe('LiveController rendering with external changes tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('will respect attribute changes to a tracked element', async () => { - const test = await createTest({ id: 'element-id', isDisabled: false, bonusClass: '', margin: '10px' }, (data: any) => ` + const test = await createTest( + { id: 'element-id', isDisabled: false, bonusClass: '', margin: '10px' }, + (data: any) => `
- `); + ` + ); // mess with the elements const button = getByTestId(test.element, 'the-button') as HTMLButtonElement; @@ -49,14 +52,13 @@ describe('LiveController rendering with external changes tests', () => { // remove a style button.style.removeProperty('border-radius'); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.isDisabled = true; - data.bonusClass = 'class-added-by-server'; - data.margin = '20px'; - data.id = 'will-be-ignored' - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.isDisabled = true; + data.bonusClass = 'class-added-by-server'; + data.margin = '20px'; + data.id = 'will-be-ignored'; + }); await test.component.render(); @@ -74,27 +76,31 @@ describe('LiveController rendering with external changes tests', () => { }); it('will not remove an added element', async () => { - const test = await createTest({ withBonusElement: false }, (data: any) => ` + const test = await createTest( + { withBonusElement: false }, + (data: any) => `
Text inside the div ${data.withBonusElement ? '
Bonus element
' : ''}
- `); + ` + ); // add a new element directly inside the root element test.element.appendChild(htmlToElement('
Added outside element
')); const innerDiv = getByTestId(test.element, 'inner-div'); // append a new element inside the inner div - innerDiv.appendChild(htmlToElement('
Added inside element append
')); + innerDiv.appendChild( + htmlToElement('
Added inside element append
') + ); // prepend a new element inside the inner div innerDiv.prepend(htmlToElement('
Added inside element prepend
')); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.withBonusElement = true; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.withBonusElement = true; + }); await test.component.render(); @@ -126,7 +132,9 @@ describe('LiveController rendering with external changes tests', () => { }); it('keeps external changes across multiple renders', async () => { - const test = await createTest({ isDisabled: false, bonusClass: '', withBonusElement: false }, (data: any) => ` + const test = await createTest( + { isDisabled: false, bonusClass: '', withBonusElement: false }, + (data: any) => `
${data.withBonusElement ? '
Bonus element
' : ''}
- `); + ` + ); // mess with the button const button = getByTestId(test.element, 'the-button') as HTMLButtonElement; @@ -143,11 +152,10 @@ describe('LiveController rendering with external changes tests', () => { const addedOutsideElement = htmlToElement('
Added outside element
'); test.element.appendChild(addedOutsideElement); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.isDisabled = true; - data.withBonusElement = true; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.isDisabled = true; + data.withBonusElement = true; + }); await test.component.render(); @@ -159,15 +167,16 @@ describe('LiveController rendering with external changes tests', () => { // make some more changes button.classList.add('externally-added-class'); button.classList.remove('originalclass2'); - const secondAddedOutsideElement = htmlToElement('
Added outside element 2
'); + const secondAddedOutsideElement = htmlToElement( + '
Added outside element 2
' + ); test.element.appendChild(secondAddedOutsideElement); addedOutsideElement.classList.add('class-added-later'); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - data.bonusClass = 'class-added-by-server'; - data.withBonusElement = false; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + data.bonusClass = 'class-added-by-server'; + data.withBonusElement = false; + }); await test.component.render(); @@ -186,4 +195,3 @@ describe('LiveController rendering with external changes tests', () => { expect(test.element.innerHTML).not.toContain('Bonus element'); }); }); - diff --git a/src/LiveComponent/assets/test/controller/render.test.ts b/src/LiveComponent/assets/test/controller/render.test.ts index c868c4ab77d..efa875c0ba9 100644 --- a/src/LiveComponent/assets/test/controller/render.test.ts +++ b/src/LiveComponent/assets/test/controller/render.test.ts @@ -15,31 +15,35 @@ import { htmlToElement } from '../../src/dom_utils'; describe('LiveController rendering Tests', () => { afterEach(() => { shutdownTests(); - }) + }); it('can re-render via an Ajax call', async () => { - const test = await createTest({ firstName: 'Ryan' }, (data: any) => ` + const test = await createTest( + { firstName: 'Ryan' }, + (data: any) => `
Name: ${data.firstName}
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.firstName = 'Kevin'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.firstName = 'Kevin'; + }); getByText(test.element, 'Reload').click(); await waitFor(() => expect(test.element).toHaveTextContent('Name: Kevin')); // data returned from the server is used for the new "data" - expect(test.component.valueStore.getOriginalProps()).toEqual({firstName: 'Kevin'}); + expect(test.component.valueStore.getOriginalProps()).toEqual({ firstName: 'Kevin' }); }); it('conserves the value of model field that was modified after a render request', async () => { - const test = await createTest({ title: 'greetings', comment: '' }, (data: any) => ` + const test = await createTest( + { title: 'greetings', comment: '' }, + (data: any) => `
@@ -115,47 +122,53 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ title: 'greetings!!' }) - .delayResponse(100); + test.expectsAjaxCall().expectUpdatedData({ title: 'greetings!!' }).delayResponse(100); userEvent.type(test.queryByDataModel('title'), '!!'); setTimeout(() => { // wait 10 ms (long enough for the shortened debounce to finish and the // Ajax request to start) and then type into this field - userEvent.type(test.element.querySelector('textarea') as HTMLTextAreaElement, 'typing after the request starts'); + userEvent.type( + test.element.querySelector('textarea') as HTMLTextAreaElement, + 'typing after the request starts' + ); }, 10); // title model updated like normal await waitFor(() => expect(test.element).toHaveTextContent('Title: "greetings!!"')); // field *still* contains the text that was typed by the user after the Ajax call started - expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts'); + expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual( + 'typing after the request starts' + ); // make a 2nd request - test.expectsAjaxCall() - .expectUpdatedData({ title: 'greetings!! Yay!' }) - .delayResponse(100); + test.expectsAjaxCall().expectUpdatedData({ title: 'greetings!! Yay!' }).delayResponse(100); userEvent.type(test.queryByDataModel('title'), ' Yay!'); // title model updated like normal await waitFor(() => expect(test.element).toHaveTextContent('Title: "greetings!! Yay!"')); // field *still* contains modified text - expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual('typing after the request starts'); + expect((test.element.querySelector('textarea') as HTMLTextAreaElement).value).toEqual( + 'typing after the request starts' + ); }); it('conserves cursor position of active model element', async () => { - const test = await createTest({ name: '' }, (data) => ` + const test = await createTest( + { name: '' }, + (data) => `
- `); + ` + ); - test.expectsAjaxCall() - .expectUpdatedData({ name: 'Hello' }); + test.expectsAjaxCall().expectUpdatedData({ name: 'Hello' }); const input = test.queryByDataModel('name') as HTMLInputElement; userEvent.type(input, 'Hello'); @@ -170,20 +183,22 @@ describe('LiveController rendering Tests', () => { }); it('uses the new value of an unmapped field that was NOT modified even if active', async () => { - const test = await createTest({ title: 'greetings' }, (data: any) => ` + const test = await createTest( + { title: 'greetings' }, + (data: any) => `
Title: "${data.title}"
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.title = 'Hello'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.title = 'Hello'; + }); const input = test.element.querySelector('input') as HTMLInputElement; // focus the input, but don't change it @@ -193,7 +208,9 @@ describe('LiveController rendering Tests', () => { }); it('does not render over elements with data-live-ignore', async () => { - const test = await createTest({ firstName: 'Ryan' }, (data: any) => ` + const test = await createTest( + { firstName: 'Ryan' }, + (data: any) => `
Inside Ignore Name: ${data.firstName}
@@ -201,29 +218,33 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); // imitate some JavaScript changing this element test.element.querySelector('span')?.setAttribute('data-foo', 'bar'); test.element.appendChild(htmlToElement('
I should not be removed
')); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.firstName = 'Kevin'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.firstName = 'Kevin'; + }); getByText(test.element, 'Reload').click(); await waitFor(() => expect(test.element).toHaveTextContent('Outside Ignore Name: Kevin')); const ignoreElement = test.element.querySelector('div[data-live-ignore]'); expect(ignoreElement).not.toBeNull(); - expect(ignoreElement?.outerHTML).toEqual('
Inside Ignore Name: Ryan
'); + expect(ignoreElement?.outerHTML).toEqual( + '
Inside Ignore Name: Ryan
' + ); expect(test.element.innerHTML).toContain('I should not be removed'); }); it('if id changes, data-live-ignore elements ARE re-rendered', async () => { - const test = await createTest({ firstName: 'Ryan', containerId: 'original' }, (data: any) => ` + const test = await createTest( + { firstName: 'Ryan', containerId: 'original' }, + (data: any) => `
Inside Ignore Name: ${data.firstName}
@@ -233,14 +254,14 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.firstName = 'Kevin'; - data.containerId = 'updated'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.firstName = 'Kevin'; + data.containerId = 'updated'; + }); getByText(test.element, 'Reload').click(); @@ -248,11 +269,15 @@ describe('LiveController rendering Tests', () => { const ignoreElement = test.element.querySelector('div[data-live-ignore]'); expect(ignoreElement).not.toBeNull(); // check that even the ignored element re-rendered - expect(ignoreElement?.outerHTML).toEqual('
Inside Ignore Name: Kevin
'); + expect(ignoreElement?.outerHTML).toEqual( + '
Inside Ignore Name: Kevin
' + ); }); it('overwrites HTML instead of morph with data-skip-morph', async () => { - const test = await createTest({ firstName: 'Ryan' }, (data: any) => ` + const test = await createTest( + { firstName: 'Ryan' }, + (data: any) => `
Inside Skip Name: ${data.firstName}
@@ -260,16 +285,16 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); const spanBefore = getByTestId(test.element, 'inside-skip-morph'); expect(spanBefore).toHaveTextContent('Ryan'); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.firstName = 'Kevin'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.firstName = 'Kevin'; + }); getByText(test.element, 'Reload').click(); @@ -287,9 +312,12 @@ describe('LiveController rendering Tests', () => { }); it('cancels a re-render if the page is navigating away', async () => { - const test = await createTest({greeting: 'aloha!'}, (data: any) => ` + const test = await createTest( + { greeting: 'aloha!' }, + (data: any) => `
${data.greeting}
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data) => { @@ -299,7 +327,7 @@ describe('LiveController rendering Tests', () => { const promise = test.component.render(); // trigger disconnect - test.element.removeAttribute('data-controller') + test.element.removeAttribute('data-controller'); // wait for the fetch to finish await promise; @@ -309,9 +337,12 @@ describe('LiveController rendering Tests', () => { }); it('renders if the page is navigating away and back', async () => { - const test = await createTest({greeting: 'aloha!'}, (data: any) => ` + const test = await createTest( + { greeting: 'aloha!' }, + (data: any) => `
${data.greeting}
- `); + ` + ); test.expectsAjaxCall() .serverWillChangeProps((data) => { @@ -322,14 +353,14 @@ describe('LiveController rendering Tests', () => { const promise = test.component.render(); // trigger controller disconnect - test.element.removeAttribute('data-controller') + test.element.removeAttribute('data-controller'); // wait for the fetch to finish await promise; - expect(test.element).toHaveTextContent('aloha!') + expect(test.element).toHaveTextContent('aloha!'); // trigger connect - test.element.setAttribute('data-controller', 'live') + test.element.setAttribute('data-controller', 'live'); test.expectsAjaxCall() .serverWillChangeProps((data) => { data.greeting = 'Hello2'; @@ -342,7 +373,9 @@ describe('LiveController rendering Tests', () => { }); it('waits for the previous request to finish & batches changes', async () => { - const test = await createTest({ title: 'greetings', contents: '' }, (data: any) => ` + const test = await createTest( + { title: 'greetings', contents: '' }, + (data: any) => `
@@ -351,11 +384,11 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); // expect the initial Reload request, but delay it - test.expectsAjaxCall() - .delayResponse(100); + test.expectsAjaxCall().delayResponse(100); getByText(test.element, 'Reload').click(); @@ -370,7 +403,7 @@ describe('LiveController rendering Tests', () => { // only 1 request, both new pieces of data sent at once .expectUpdatedData({ title: 'greetings!!!', - contents: 'Welcome to our test!' + contents: 'Welcome to our test!', }); }, 10); @@ -378,7 +411,9 @@ describe('LiveController rendering Tests', () => { }); it('batches re-render requests together that occurred during debounce', async () => { - const test = await createTest({ title: 'greetings', contents: '' }, (data: any) => ` + const test = await createTest( + { title: 'greetings', contents: '' }, + (data: any) => `
@@ -387,7 +422,8 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); // type: 50ms debounce will begin userEvent.type(test.queryByDataModel('title'), ' to you'); @@ -399,19 +435,23 @@ describe('LiveController rendering Tests', () => { // delay 40 ms before we start to expect it setTimeout(() => { // just one request should be made - test.expectsAjaxCall() - .expectUpdatedData({ title: 'greetings to you', contents: 'Welcome to our test!'}); - }, 40) + test.expectsAjaxCall().expectUpdatedData({ + title: 'greetings to you', + contents: 'Welcome to our test!', + }); + }, 40); }, 40); await waitFor(() => expect(test.element).toHaveTextContent('Title: "greetings to you"')); }); it('waits for the rendering process of previous request to finish before starting a new one', async () => { - const test = await createTest({ - title: 'greetings', - contents: '', - }, (data: any) => ` + const test = await createTest( + { + title: 'greetings', + contents: '', + }, + (data: any) => `
@@ -419,7 +459,8 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); let didSecondRenderStart = false; let secondRenderStartedAt = 0; @@ -452,7 +493,7 @@ describe('LiveController rendering Tests', () => { const sleep = (milliseconds: number) => { const startTime = new Date().getTime(); while (new Date().getTime() < startTime + milliseconds); - } + }; sleep(10); }); @@ -467,27 +508,31 @@ describe('LiveController rendering Tests', () => { }); it('can update svg', async () => { - const test = await createTest({ text: 'SVG' }, (data: any) => ` + const test = await createTest( + { text: 'SVG' }, + (data: any) => `
${data.text}
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.text = '123'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.text = '123'; + }); getByText(test.element, 'Reload').click(); await waitFor(() => expect(test.element).toHaveTextContent('123')); }); it('can update html containing svg', async () => { - const test = await createTest({text: 'Hello'}, (data: any) => ` + const test = await createTest( + { text: 'Hello' }, + (data: any) => `
${data.text} @@ -495,13 +540,13 @@ describe('LiveController rendering Tests', () => {
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data: any) => { - // change the data on the server so the template renders differently - data.text = '123'; - }); + test.expectsAjaxCall().serverWillChangeProps((data: any) => { + // change the data on the server so the template renders differently + data.text = '123'; + }); getByText(test.element, 'Reload').click(); @@ -509,17 +554,19 @@ describe('LiveController rendering Tests', () => { }); it('can understand comment in the response', async () => { - const test = await createTest({ season: 'summer' }, (data: any) => ` + const test = await createTest( + { season: 'summer' }, + (data: any) => `
The season is: ${data.season}
- `); + ` + ); - test.expectsAjaxCall() - .serverWillChangeProps((data) => { - data.season = 'autumn'; - }); + test.expectsAjaxCall().serverWillChangeProps((data) => { + data.season = 'autumn'; + }); await test.component.render(); // verify the component *did* render ok @@ -527,7 +574,9 @@ describe('LiveController rendering Tests', () => { }); it('select the placeholder option tag after render', async () => { - const test = await createTest({}, (data: any) => ` + const test = await createTest( + {}, + (data: any) => `
- `); + ` + ); - test.expectsAjaxCall() - .willReturn((data) => ` + test.expectsAjaxCall().willReturn( + (data) => `
- `); + ` + ); await test.component.render(); const selectOption2 = test.element.querySelector('#select_option_2') as HTMLSelectElement; diff --git a/src/LiveComponent/assets/test/dom_utils.test.ts b/src/LiveComponent/assets/test/dom_utils.test.ts index a85c936f339..2afdbafd132 100644 --- a/src/LiveComponent/assets/test/dom_utils.test.ts +++ b/src/LiveComponent/assets/test/dom_utils.test.ts @@ -4,14 +4,14 @@ import { htmlToElement, getModelDirectiveFromElement, elementBelongsToThisComponent, - setValueOnElement + setValueOnElement, } from '../src/dom_utils'; import ValueStore from '../src/Component/ValueStore'; import Component from '../src/Component'; import Backend from '../src/Backend/Backend'; import { noopElementDriver } from './tools'; -const createStore = (props: any = {}): ValueStore => new ValueStore(props) +const createStore = (props: any = {}): ValueStore => new ValueStore(props); describe('getValueFromElement', () => { it('Correctly adds data from valued checked checkbox', () => { @@ -21,17 +21,13 @@ describe('getValueFromElement', () => { input.dataset.model = 'foo'; input.value = 'the_checkbox_value'; - expect(getValueFromElement(input, createStore())) - .toEqual('the_checkbox_value'); + expect(getValueFromElement(input, createStore())).toEqual('the_checkbox_value'); - expect(getValueFromElement(input, createStore({ foo: [] }))) - .toEqual(['the_checkbox_value']); + expect(getValueFromElement(input, createStore({ foo: [] }))).toEqual(['the_checkbox_value']); - expect(getValueFromElement(input, createStore({ foo: ['bar'] }))) - .toEqual(['bar', 'the_checkbox_value']); + expect(getValueFromElement(input, createStore({ foo: ['bar'] }))).toEqual(['bar', 'the_checkbox_value']); - expect(getValueFromElement(input, createStore({ foo: {'1': 'bar'} }))) - .toEqual(['bar', 'the_checkbox_value']); + expect(getValueFromElement(input, createStore({ foo: { '1': 'bar' } }))).toEqual(['bar', 'the_checkbox_value']); }); it('Correctly removes data from valued unchecked checkbox', () => { @@ -41,17 +37,14 @@ describe('getValueFromElement', () => { input.dataset.model = 'foo'; input.value = 'the_checkbox_value'; - expect(getValueFromElement(input, createStore())) - .toEqual(null); - expect(getValueFromElement(input, createStore({ foo: ['the_checkbox_value'] }))) - .toEqual([]); + expect(getValueFromElement(input, createStore())).toEqual(null); + expect(getValueFromElement(input, createStore({ foo: ['the_checkbox_value'] }))).toEqual([]); // unchecked value already was not in store - expect(getValueFromElement(input, createStore({ foo: ['bar'] }))) - .toEqual(['bar']); - expect(getValueFromElement(input, createStore({ foo: ['bar', 'the_checkbox_value'] }))) - .toEqual(['bar']); - expect(getValueFromElement(input, createStore({ foo: {'1': 'bar', '2': 'the_checkbox_value'} }))) - .toEqual(['bar']); + expect(getValueFromElement(input, createStore({ foo: ['bar'] }))).toEqual(['bar']); + expect(getValueFromElement(input, createStore({ foo: ['bar', 'the_checkbox_value'] }))).toEqual(['bar']); + expect(getValueFromElement(input, createStore({ foo: { '1': 'bar', '2': 'the_checkbox_value' } }))).toEqual([ + 'bar', + ]); }); it('Correctly handles boolean checkbox', () => { @@ -60,13 +53,11 @@ describe('getValueFromElement', () => { input.checked = true; input.dataset.model = 'foo'; - expect(getValueFromElement(input, createStore())) - .toEqual(true); + expect(getValueFromElement(input, createStore())).toEqual(true); input.checked = false; - expect(getValueFromElement(input, createStore())) - .toEqual(false); + expect(getValueFromElement(input, createStore())).toEqual(false); }); it('Correctly returns for non-model checkboxes', () => { @@ -75,12 +66,10 @@ describe('getValueFromElement', () => { input.checked = true; input.value = 'the_checkbox_value'; - expect(getValueFromElement(input, createStore())) - .toEqual('the_checkbox_value'); + expect(getValueFromElement(input, createStore())).toEqual('the_checkbox_value'); input.checked = false; - expect(getValueFromElement(input, createStore())) - .toEqual(null); + expect(getValueFromElement(input, createStore())).toEqual(null); }); it('Correctly sets data from select multiple', () => { @@ -95,32 +84,27 @@ describe('getValueFromElement', () => { select.add(barOption); // nothing selected - expect(getValueFromElement(select, createStore())) - .toEqual([]); + expect(getValueFromElement(select, createStore())).toEqual([]); fooOption.selected = true; - expect(getValueFromElement(select, createStore())) - .toEqual(['foo']); + expect(getValueFromElement(select, createStore())).toEqual(['foo']); barOption.selected = true; - expect(getValueFromElement(select, createStore())) - .toEqual(['foo', 'bar']); - }) + expect(getValueFromElement(select, createStore())).toEqual(['foo', 'bar']); + }); it('Grabs data-value attribute for other elements', () => { const div = document.createElement('div'); div.dataset.value = 'the_value'; - expect(getValueFromElement(div, createStore())) - .toEqual('the_value'); + expect(getValueFromElement(div, createStore())).toEqual('the_value'); }); it('Grabs value attribute for other elements', () => { const div = document.createElement('div'); div.setAttribute('value', 'the_value_from_attribute'); - expect(getValueFromElement(div, createStore())) - .toEqual('the_value_from_attribute'); + expect(getValueFromElement(div, createStore())).toEqual('the_value_from_attribute'); }); }); @@ -207,7 +191,7 @@ describe('setValueOnElement', () => { setValueOnElement(select, ['foo']); expect(fooOption.selected).toBeTruthy(); expect(barOption.selected).toBeFalsy(); - }) + }); it('Sets value on other elements', () => { const input = document.createElement('input'); @@ -258,7 +242,9 @@ describe('getModelDirectiveFromInput', () => { it('throws error if no data-model found', () => { const element = htmlToElement(''); - expect(() => { getModelDirectiveFromElement(element) }).toThrow('Cannot determine the model name'); + expect(() => { + getModelDirectiveFromElement(element); + }).toThrow('Cannot determine the model name'); }); }); @@ -271,7 +257,7 @@ describe('elementBelongsToThisComponent', () => { [], null, new Backend(''), - new noopElementDriver(), + new noopElementDriver() ); component.connect(); diff --git a/src/LiveComponent/assets/test/normalize_attributes_for_comparison.test.ts b/src/LiveComponent/assets/test/normalize_attributes_for_comparison.test.ts index c2ac75a6581..bce5d55f057 100644 --- a/src/LiveComponent/assets/test/normalize_attributes_for_comparison.test.ts +++ b/src/LiveComponent/assets/test/normalize_attributes_for_comparison.test.ts @@ -5,48 +5,46 @@ describe('normalizeAttributesForComparison', () => { it('makes no changes if value and attribute not set', () => { const element = htmlToElement('
'); normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual('
'); + expect(element.outerHTML).toEqual('
'); }); it('sets the attribute if the value is present', () => { const element = htmlToElement('') as HTMLInputElement; element.value = 'set value'; normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual(''); + expect(element.outerHTML).toEqual(''); }); it('sets the attribute to empty if the value is empty', () => { const element = htmlToElement('') as HTMLInputElement; element.value = ''; normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual(''); + expect(element.outerHTML).toEqual(''); }); it('changes the value attribute if value is different', () => { const element = htmlToElement('') as HTMLInputElement; element.value = 'changed'; normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual(''); + expect(element.outerHTML).toEqual(''); }); it('changes the value attribute on a child', () => { const element = htmlToElement('
'); (element.querySelector('#child') as HTMLInputElement).value = 'changed'; normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual('
'); + expect(element.outerHTML).toEqual('
'); }); it('changes the value on multiple levels', () => { - const element = htmlToElement('
'); + const element = htmlToElement( + '
' + ); (element.querySelector('#child') as HTMLInputElement).value = 'changed'; (element.querySelector('#grand_child') as HTMLInputElement).value = 'changed grand child'; normalizeAttributesForComparison(element); - expect(element.outerHTML) - .toEqual('
'); + expect(element.outerHTML).toEqual( + '
' + ); }); }); diff --git a/src/LiveComponent/assets/test/tools.ts b/src/LiveComponent/assets/test/tools.ts index 5c72acdbc32..4b0f67993d4 100644 --- a/src/LiveComponent/assets/test/tools.ts +++ b/src/LiveComponent/assets/test/tools.ts @@ -32,11 +32,11 @@ export function shutdownTests() { } const shutdownTest = (test: FunctionalTest) => { - test.pendingAjaxCallsThatAreStillExpected().forEach((mock => { + test.pendingAjaxCallsThatAreStillExpected().forEach((mock) => { const requestInfo = mock.getVisualSummary(); throw new Error(`EXPECTED request was never made matching the following info: \n${requestInfo.join('\n')}`); - })); -} + }); +}; class FunctionalTest { component: Component; @@ -44,7 +44,12 @@ class FunctionalTest { template: (props: any) => string; mockedBackend: MockedBackend; - constructor(component: Component, element: HTMLElement, mockedBackend: MockedBackend, template: (props: any) => string) { + constructor( + component: Component, + element: HTMLElement, + mockedBackend: MockedBackend, + template: (props: any) => string + ) { this.component = component; this.element = element; this.mockedBackend = mockedBackend; @@ -56,11 +61,11 @@ class FunctionalTest { this.mockedBackend.addMockedAjaxCall(mock); return mock; - } + }; queryByDataModel(modelName: string): HTMLElement { const elements = this.element.querySelectorAll(`[data-model$="${modelName}"]`); - let matchedElement: null|Element = null; + let matchedElement: null | Element = null; // skip any elements that are actually controllers // these are child component bindings, not real fields @@ -97,7 +102,13 @@ class MockedBackend implements BackendInterface { this.expectedMockedAjaxCalls.push(mock); } - makeRequest(props: any, actions: BackendAction[], updated: { [key: string]: any }, children: ChildrenFingerprints, updatedPropsFromParent: {[key: string]: any}): BackendRequest { + makeRequest( + props: any, + actions: BackendAction[], + updated: { [key: string]: any }, + children: ChildrenFingerprints, + updatedPropsFromParent: { [key: string]: any } + ): BackendRequest { const matchedMock = this.findMatchingMock(props, actions, updated, children, updatedPropsFromParent); if (!matchedMock) { @@ -114,7 +125,7 @@ class MockedBackend implements BackendInterface { requestInfo.push('No mocked Ajax calls were expected.'); } else { this.expectedMockedAjaxCalls.forEach((mock) => { - requestInfo.push(`EXPECTED REQUEST #${this.expectedMockedAjaxCalls.indexOf(mock) + 1}:`) + requestInfo.push(`EXPECTED REQUEST #${this.expectedMockedAjaxCalls.indexOf(mock) + 1}:`); requestInfo.push(...mock.getVisualSummary()); }); } @@ -132,8 +143,14 @@ class MockedBackend implements BackendInterface { return this.expectedMockedAjaxCalls; } - private findMatchingMock(props: any, actions: BackendAction[], updated: { [key: string]: any }, children: ChildrenFingerprints, updatedPropsFromParent: {[key: string]: any}): MockedAjaxCall|null { - for(let i = 0; i < this.expectedMockedAjaxCalls.length; i++) { + private findMatchingMock( + props: any, + actions: BackendAction[], + updated: { [key: string]: any }, + children: ChildrenFingerprints, + updatedPropsFromParent: { [key: string]: any } + ): MockedAjaxCall | null { + for (let i = 0; i < this.expectedMockedAjaxCalls.length; i++) { const mock = this.expectedMockedAjaxCalls[i]; if (mock.matches(props, actions, updated, children, updatedPropsFromParent)) { return mock; @@ -148,14 +165,14 @@ class MockedAjaxCall { private test: FunctionalTest; /* Matcher properties */ - private expectedActions: Array<{ name: string, args: any }> = []; + private expectedActions: Array<{ name: string; args: any }> = []; private expectedSentUpdatedData: { [key: string]: any } = {}; - private expectedChildFingerprints: ChildrenFingerprints|null = null; - private expectedUpdatedPropsFromParent: {[key: string]: any}|null = null; + private expectedChildFingerprints: ChildrenFingerprints | null = null; + private expectedUpdatedPropsFromParent: { [key: string]: any } | null = null; /* Response properties */ private changePropsCallback?: (props: any) => void; - private template?: (props: any) => string + private template?: (props: any) => string; private delayResponseTime?: number = 0; private customResponseStatusCode?: number; private customResponseHTML?: string; @@ -175,7 +192,13 @@ class MockedAjaxCall { return requestInfo; } - matches(props: any, actions: BackendAction[], updated: { [key: string]: any }, children: ChildrenFingerprints, updatedPropsFromParent: {[key: string]: any}): boolean { + matches( + props: any, + actions: BackendAction[], + updated: { [key: string]: any }, + children: ChildrenFingerprints, + updatedPropsFromParent: { [key: string]: any } + ): boolean { if (!this.isEqual(this.test.component.valueStore.getOriginalProps(), props)) { return false; } @@ -184,7 +207,7 @@ class MockedAjaxCall { return { name: action.name, args: action.args, - } + }; }); if (!this.isEqual(normalizedBackendActions, this.expectedActions)) { @@ -195,15 +218,14 @@ class MockedAjaxCall { return false; } - if ( - null !== this.expectedChildFingerprints && !this.isEqual(children, this.expectedChildFingerprints) - ) { + if (null !== this.expectedChildFingerprints && !this.isEqual(children, this.expectedChildFingerprints)) { return false; } if ( - (null !== this.expectedUpdatedPropsFromParent || Object.keys(updatedPropsFromParent).length > 0) - && !this.isEqual(updatedPropsFromParent, this.expectedUpdatedPropsFromParent)) { + (null !== this.expectedUpdatedPropsFromParent || Object.keys(updatedPropsFromParent).length > 0) && + !this.isEqual(updatedPropsFromParent, this.expectedUpdatedPropsFromParent) + ) { return false; } @@ -254,7 +276,7 @@ class MockedAjaxCall { const response = new Response(html, { status: this.customResponseStatusCode || 200, - headers + headers, }); resolve(response); @@ -265,7 +287,7 @@ class MockedAjaxCall { // @ts-ignore Response doesn't quite match the underlying interface promise, this.expectedActions.map((action) => action.name), - Object.keys(this.expectedSentUpdatedData), + Object.keys(this.expectedSentUpdatedData) ); } @@ -276,19 +298,19 @@ class MockedAjaxCall { this.expectedSentUpdatedData = updated; return this; - } + }; expectChildFingerprints = (fingerprints: any): MockedAjaxCall => { this.expectedChildFingerprints = fingerprints; return this; - } + }; expectUpdatedPropsFromParent = (updatedProps: any): MockedAjaxCall => { this.expectedUpdatedPropsFromParent = updatedProps; return this; - } + }; /** * Call if the "server" will change the props before it re-renders. @@ -297,19 +319,19 @@ class MockedAjaxCall { this.changePropsCallback = callback; return this; - } + }; delayResponse = (milliseconds: number): MockedAjaxCall => { this.delayResponseTime = milliseconds; return this; - } + }; expectActionCalled(actionName: string, args: any = {}): MockedAjaxCall { this.expectedActions.push({ name: actionName, - args: args - }) + args: args, + }); return this; } @@ -336,15 +358,19 @@ class MockedAjaxCall { return a === b; } - const sortedA = Object.keys(a).sort().reduce((obj: any, key) => { - obj[key] = a[key]; - return obj; - }, {}); + const sortedA = Object.keys(a) + .sort() + .reduce((obj: any, key) => { + obj[key] = a[key]; + return obj; + }, {}); - const sortedB = Object.keys(b).sort().reduce((obj: any, key) => { - obj[key] = b[key]; - return obj; - }, {}); + const sortedB = Object.keys(b) + .sort() + .reduce((obj: any, key) => { + obj[key] = b[key]; + return obj; + }, {}); return JSON.stringify(sortedA) === JSON.stringify(sortedB); } @@ -372,7 +398,7 @@ export function createTestForExistingComponent(component: Component): Functional return test; } -export async function startStimulus(element: string|HTMLElement) { +export async function startStimulus(element: string | HTMLElement) { // start the Stimulus app just once per test suite if (application) { await application.start(); @@ -395,12 +421,12 @@ export async function startStimulus(element: string|HTMLElement) { return { controller, element: controllerElement, - } + }; } export const getStimulusApplication = (): Application => { return application; -} +}; const getControllerElement = (container: HTMLElement): HTMLElement => { if (container.dataset.controller === 'live') { @@ -427,8 +453,8 @@ export const dataToJsonAttribute = (data: any): string => { } // returns the now-escaped string, ready to be used in an HTML attribute - return matches[1] -} + return matches[1]; +}; export function initComponent(props: any = {}, controllerValues: any = {}) { return ` @@ -447,7 +473,7 @@ export function initComponent(props: any = {}, controllerValues: any = {}) { `; } -export function getComponent(element: HTMLElement|null) { +export function getComponent(element: HTMLElement | null) { if (!element) { throw new Error('could not find element'); } @@ -461,11 +487,11 @@ export function getComponent(element: HTMLElement|null) { return component; } -export function setCurrentSearch(search: string){ +export function setCurrentSearch(search: string) { history.replaceState(history.state, '', window.location.origin + window.location.pathname + search); } -export function expectCurrentSearch (){ +export function expectCurrentSearch() { return expect(decodeURIComponent(window.location.search)); } @@ -482,7 +508,7 @@ export class noopElementDriver implements ElementDriver { event: string; data: any; target: string | null; - componentName: string | null + componentName: string | null; }> { throw new Error('Method not implemented.'); } diff --git a/src/LiveComponent/assets/test/url_utils.test.ts b/src/LiveComponent/assets/test/url_utils.test.ts index 3813bacc3f5..fcb711f59cc 100644 --- a/src/LiveComponent/assets/test/url_utils.test.ts +++ b/src/LiveComponent/assets/test/url_utils.test.ts @@ -16,7 +16,7 @@ describe('url_utils', () => { beforeEach(() => { // Reset search before each test - urlUtils.search = ''; + urlUtils.search = ''; }); it('set the param if it does not exist', () => { @@ -92,7 +92,7 @@ describe('url_utils', () => { }); it('keep other params unchanged', () => { - urlUtils.search ='?param=foo&otherParam=bar'; + urlUtils.search = '?param=foo&otherParam=bar'; urlUtils.remove('param'); @@ -107,8 +107,8 @@ describe('url_utils', () => { expect(urlUtils.search).toEqual(''); }); - it ('remove all occurrences of an object param', () => { - urlUtils.search ='?param[foo]=1¶m[bar]=baz'; + it('remove all occurrences of an object param', () => { + urlUtils.search = '?param[foo]=1¶m[bar]=baz'; urlUtils.remove('param'); @@ -122,13 +122,13 @@ describe('url_utils', () => { beforeAll(() => { initialUrl = new URL(window.location.href); }); - afterEach(()=> { + afterEach(() => { history.replaceState(history.state, '', initialUrl); }); it('replace URL', () => { - const newUrl = new URL(`${window.location.href}/foo/bar`); - HistoryStrategy.replace(newUrl); - expect(window.location.href).toEqual(newUrl.toString()); + const newUrl = new URL(`${window.location.href}/foo/bar`); + HistoryStrategy.replace(newUrl); + expect(window.location.href).toEqual(newUrl.toString()); }); - }) + }); }); diff --git a/src/React/assets/test/register_controller.test.tsx b/src/React/assets/test/register_controller.test.tsx index ef723ec463c..756de1cdba4 100644 --- a/src/React/assets/test/register_controller.test.tsx +++ b/src/React/assets/test/register_controller.test.tsx @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import {registerReactControllerComponents} from '../src/register_controller'; +import { registerReactControllerComponents } from '../src/register_controller'; import MyTsxComponent from './fixtures/MyTsxComponent'; // @ts-ignore import MyJsxComponent from './fixtures/MyJsxComponent'; @@ -41,6 +41,8 @@ describe('registerReactControllerComponents', () => { registerReactControllerComponents(createFakeFixturesContext()); const resolveComponent = (window as any).resolveReactComponent; - expect(() => resolveComponent('MyABCComponent')).toThrow('React controller "MyABCComponent" does not exist. Possible values: MyJsxComponent, MyTsxComponent'); + expect(() => resolveComponent('MyABCComponent')).toThrow( + 'React controller "MyABCComponent" does not exist. Possible values: MyJsxComponent, MyTsxComponent' + ); }); }); diff --git a/src/StimulusBundle/assets/test/loader.test.ts b/src/StimulusBundle/assets/test/loader.test.ts index 4e0a1f99153..cd7f884fe55 100644 --- a/src/StimulusBundle/assets/test/loader.test.ts +++ b/src/StimulusBundle/assets/test/loader.test.ts @@ -2,10 +2,7 @@ // which does not actually exist in the source code import { loadControllers } from '../dist/loader'; import { Application, Controller } from '@hotwired/stimulus'; -import type { - EagerControllersCollection, - LazyControllersCollection, -} from '../src/controllers'; +import type { EagerControllersCollection, LazyControllersCollection } from '../src/controllers'; import { waitFor } from '@testing-library/dom'; let isController1Initialized = false; @@ -52,7 +49,7 @@ describe('loader', () => { document.body.innerHTML = '
'; // wait a moment for the MutationObserver to fire - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(isController3Initialized).toBe(true); application.stop(); diff --git a/src/Svelte/assets/test/fixtures/MyComponent.svelte b/src/Svelte/assets/test/fixtures/MyComponent.svelte index e78fdbf27a9..c5f9d4eb515 100644 --- a/src/Svelte/assets/test/fixtures/MyComponent.svelte +++ b/src/Svelte/assets/test/fixtures/MyComponent.svelte @@ -1,6 +1,6 @@
diff --git a/src/Svelte/assets/test/register_controller.test.ts b/src/Svelte/assets/test/register_controller.test.ts index 7909198f084..35eb6e333a7 100644 --- a/src/Svelte/assets/test/register_controller.test.ts +++ b/src/Svelte/assets/test/register_controller.test.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import {registerSvelteControllerComponents} from '../src/register_controller'; +import { registerSvelteControllerComponents } from '../src/register_controller'; import MyComponent from './fixtures/MyComponent.svelte'; import RequireContext = __WebpackModuleApi.RequireContext; diff --git a/src/Svelte/assets/test/render_controller.test.ts b/src/Svelte/assets/test/render_controller.test.ts index 42c79f4a8b2..b2d3fbe5130 100644 --- a/src/Svelte/assets/test/render_controller.test.ts +++ b/src/Svelte/assets/test/render_controller.test.ts @@ -66,7 +66,6 @@ describe('SvelteController', () => { }); it('connect without props', async () => { - const container = mountDOM(`
{ const application = Application.start(); application.register('check', CheckController); application.register('toggle-password', TogglePasswordController); -} +}; describe('TogglePasswordController', () => { let container: HTMLElement; diff --git a/src/Translator/assets/dist/translator_controller.js b/src/Translator/assets/dist/translator_controller.js index f85f449248a..0dbb57935fe 100644 --- a/src/Translator/assets/dist/translator_controller.js +++ b/src/Translator/assets/dist/translator_controller.js @@ -56,9 +56,11 @@ function format(id, parameters, locale) { } else { const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left); - const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right); - if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) - && (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) { + const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) + ? Number.POSITIVE_INFINITY + : Number(matchGroups.right); + if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) && + (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) { return strtr(matchGroups.message, parameters); } } @@ -137,7 +139,7 @@ function getPluralizationRule(number, locale) { case 'tk': case 'ur': case 'zu': - return (1 === number) ? 0 : 1; + return 1 === number ? 0 : 1; case 'am': case 'bh': case 'fil': @@ -151,7 +153,7 @@ function getPluralizationRule(number, locale) { case 'pt_BR': case 'ti': case 'wa': - return (number < 2) ? 0 : 1; + return number < 2 ? 0 : 1; case 'be': case 'bs': case 'hr': @@ -159,30 +161,58 @@ function getPluralizationRule(number, locale) { case 'sh': case 'sr': case 'uk': - return ((1 === number % 10) && (11 !== number % 100)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); + return 1 === number % 10 && 11 !== number % 100 + ? 0 + : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) + ? 1 + : 2; case 'cs': case 'sk': - return (1 === number) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2); + return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2; case 'ga': - return (1 === number) ? 0 : ((2 === number) ? 1 : 2); + return 1 === number ? 0 : 2 === number ? 1 : 2; case 'lt': - return ((1 === number % 10) && (11 !== number % 100)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); + return 1 === number % 10 && 11 !== number % 100 + ? 0 + : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) + ? 1 + : 2; case 'sl': - return (1 === number % 100) ? 0 : ((2 === number % 100) ? 1 : (((3 === number % 100) || (4 === number % 100)) ? 2 : 3)); + return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3; case 'mk': - return (1 === number % 10) ? 0 : 1; + return 1 === number % 10 ? 0 : 1; case 'mt': - return (1 === number) ? 0 : (((0 === number) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3)); + return 1 === number + ? 0 + : 0 === number || (number % 100 > 1 && number % 100 < 11) + ? 1 + : number % 100 > 10 && number % 100 < 20 + ? 2 + : 3; case 'lv': - return (0 === number) ? 0 : (((1 === number % 10) && (11 !== number % 100)) ? 1 : 2); + return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2; case 'pl': - return (1 === number) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2); + return 1 === number + ? 0 + : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) + ? 1 + : 2; case 'cy': - return (1 === number) ? 0 : ((2 === number) ? 1 : (((8 === number) || (11 === number)) ? 2 : 3)); + return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3; case 'ro': - return (1 === number) ? 0 : (((0 === number) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2); + return 1 === number ? 0 : 0 === number || (number % 100 > 0 && number % 100 < 20) ? 1 : 2; case 'ar': - return (0 === number) ? 0 : ((1 === number) ? 1 : ((2 === number) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5)))); + return 0 === number + ? 0 + : 1 === number + ? 1 + : 2 === number + ? 2 + : number % 100 >= 3 && number % 100 <= 10 + ? 3 + : number % 100 >= 11 && number % 100 <= 99 + ? 4 + : 5; default: return 0; } diff --git a/src/Translator/assets/src/formatters/formatter.ts b/src/Translator/assets/src/formatters/formatter.ts index 0b49a9633cd..80d99b828a6 100644 --- a/src/Translator/assets/src/formatters/formatter.ts +++ b/src/Translator/assets/src/formatters/formatter.ts @@ -1,4 +1,4 @@ -import {strtr} from '../utils'; +import { strtr } from '../utils'; /** * This code is adapted from the Symfony Translator Trait (6.2) @@ -65,7 +65,8 @@ export function format(id: string, parameters: Record, parts = id.match(/(?:\|\||[^|])+/g) || []; } - const intervalRegex = /^(?({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?[[\]])\s*(?-Inf|-?\d+(\.\d+)?)\s*,\s*(?\+?Inf|-?\d+(\.\d+)?)\s*(?[[\]]))\s*(?.*?)$/s; + const intervalRegex = + /^(?({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?[[\]])\s*(?-Inf|-?\d+(\.\d+)?)\s*,\s*(?\+?Inf|-?\d+(\.\d+)?)\s*(?[[\]]))\s*(?.*?)$/s; const standardRules: Array = []; for (let part of parts) { @@ -85,10 +86,13 @@ export function format(id: string, parameters: Record, } } else { const leftNumber = '-Inf' === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left); - const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right); + const rightNumber = ['Inf', '+Inf'].includes(matchGroups.right) + ? Number.POSITIVE_INFINITY + : Number(matchGroups.right); - if (('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) - && (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber) + if ( + ('[' === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) && + (']' === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber) ) { return strtr(matchGroups.message, parameters); } @@ -107,7 +111,9 @@ export function format(id: string, parameters: Record, return strtr(standardRules[0], parameters); } - throw new Error(`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`) + throw new Error( + `Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").` + ); } return strtr(standardRules[position], parameters); @@ -183,7 +189,7 @@ function getPluralizationRule(number: number, locale: string): number { case 'tk': case 'ur': case 'zu': - return (1 === number) ? 0 : 1; + return 1 === number ? 0 : 1; case 'am': case 'bh': case 'fil': @@ -197,7 +203,7 @@ function getPluralizationRule(number: number, locale: string): number { case 'pt_BR': case 'ti': case 'wa': - return (number < 2) ? 0 : 1; + return number < 2 ? 0 : 1; case 'be': case 'bs': case 'hr': @@ -205,31 +211,59 @@ function getPluralizationRule(number: number, locale: string): number { case 'sh': case 'sr': case 'uk': - return ((1 === number % 10) && (11 !== number % 100)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); + return 1 === number % 10 && 11 !== number % 100 + ? 0 + : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) + ? 1 + : 2; case 'cs': case 'sk': - return (1 === number) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2); + return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2; case 'ga': - return (1 === number) ? 0 : ((2 === number) ? 1 : 2); + return 1 === number ? 0 : 2 === number ? 1 : 2; case 'lt': - return ((1 === number % 10) && (11 !== number % 100)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); + return 1 === number % 10 && 11 !== number % 100 + ? 0 + : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) + ? 1 + : 2; case 'sl': - return (1 === number % 100) ? 0 : ((2 === number % 100) ? 1 : (((3 === number % 100) || (4 === number % 100)) ? 2 : 3)); + return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3; case 'mk': - return (1 === number % 10) ? 0 : 1; + return 1 === number % 10 ? 0 : 1; case 'mt': - return (1 === number) ? 0 : (((0 === number) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3)); + return 1 === number + ? 0 + : 0 === number || (number % 100 > 1 && number % 100 < 11) + ? 1 + : number % 100 > 10 && number % 100 < 20 + ? 2 + : 3; case 'lv': - return (0 === number) ? 0 : (((1 === number % 10) && (11 !== number % 100)) ? 1 : 2); + return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2; case 'pl': - return (1 === number) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2); + return 1 === number + ? 0 + : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) + ? 1 + : 2; case 'cy': - return (1 === number) ? 0 : ((2 === number) ? 1 : (((8 === number) || (11 === number)) ? 2 : 3)); + return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3; case 'ro': - return (1 === number) ? 0 : (((0 === number) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2); + return 1 === number ? 0 : 0 === number || (number % 100 > 0 && number % 100 < 20) ? 1 : 2; case 'ar': - return (0 === number) ? 0 : ((1 === number) ? 1 : ((2 === number) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5)))); + return 0 === number + ? 0 + : 1 === number + ? 1 + : 2 === number + ? 2 + : number % 100 >= 3 && number % 100 <= 10 + ? 3 + : number % 100 >= 11 && number % 100 <= 99 + ? 4 + : 5; default: - return 0 + return 0; } } diff --git a/src/Translator/assets/test/formatters/formatter.test.ts b/src/Translator/assets/test/formatters/formatter.test.ts index 9f645529120..e8c58427789 100644 --- a/src/Translator/assets/test/formatters/formatter.test.ts +++ b/src/Translator/assets/test/formatters/formatter.test.ts @@ -7,12 +7,12 @@ * file that was distributed with this source code. */ -import {format} from '../../src/formatters/formatter'; +import { format } from '../../src/formatters/formatter'; describe('Formatter', () => { test.concurrent.each<[string, string, Record]>([ ['Symfony is great!', 'Symfony is great!', {}], - ['Symfony is awesome!', 'Symfony is %what%!', {'%what%': 'awesome'}], + ['Symfony is awesome!', 'Symfony is %what%!', { '%what%': 'awesome' }], ])('#%# format should returns %p', (expected, message, parameters) => { expect(format(message, parameters, 'en')).toEqual(expected); }); @@ -27,7 +27,7 @@ describe('Formatter', () => { // custom validation messages may be coded with a fixed value ['There are 2 apples', 'There are 2 apples', 2], ])('#%# format with choice should returns %p', (expected, message, number) => { - expect(format(message, {'%count%': number}, 'en')).toEqual(expected); + expect(format(message, { '%count%': number }, 'en')).toEqual(expected); }); test.concurrent.each<[string, number, string]>([ @@ -41,7 +41,7 @@ describe('Formatter', () => { ['foo', Math.log(0), '[-Inf,2['], ['foo', -Math.log(0), '[-2,+Inf]'], ])('#%# format interval should returns %p', (expected, number, interval) => { - expect(format(`${interval} foo|[1,Inf[ bar`, {'%count%': number}, 'en')).toEqual(expected); + expect(format(`${interval} foo|[1,Inf[ bar`, { '%count%': number }, 'en')).toEqual(expected); }); test.concurrent.each<[string, number]>([ @@ -50,19 +50,29 @@ describe('Formatter', () => { ['{1} There is one apple|]2,Inf] There are %count% apples', 2], ['{0} There are no apples|There is one apple', 2], ])('#%# test non matching message', (message, number) => { - expect(() => format(message, {'%count%': number}, 'en')).toThrow(`Unable to choose a translation for "${message}" with locale "en" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`); - }) + expect(() => format(message, { '%count%': number }, 'en')).toThrow( + `Unable to choose a translation for "${message}" with locale "en" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").` + ); + }); test.concurrent.each([ ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], - ['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], + [ + 'There are no apples', + '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', + 0, + ], ['There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0], ['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10], ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10], - ['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10], + [ + 'There are 10 apples', + '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', + 10, + ], ['There are 0 apples', 'There is one apple|There are %count% apples', 0], ['There is one apple', 'There is one apple|There are %count% apples', 1], @@ -85,46 +95,102 @@ describe('Formatter', () => { ['There are 2 apples', 'There is one apple|There are %count% apples', 2], // Tests for float numbers - ['There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7], - ['There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1], - ['There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7], - ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0], - ['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0], - ['There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0], + [ + 'There is almost one apple', + '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', + 0.7, + ], + [ + 'There is one apple', + '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', + 1, + ], + [ + 'There is more than one apple', + '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', + 1.7, + ], + [ + 'There are no apples', + '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', + 0, + ], + [ + 'There are no apples', + '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', + 0.0, + ], + [ + 'There are no apples', + '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', + 0, + ], // Test texts with new-lines // with double-quotes and \n in id & double-quotes and actual newlines in text - ['This is a text with a\n new-line in it. Selector = 0.', `{0}This is a text with a + [ + 'This is a text with a\n new-line in it. Selector = 0.', + `{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.`, 0], + new-line in it. Selector > 1.`, + 0, + ], // with double-quotes and \n in id and single-quotes and actual newlines in text - ['This is a text with a\n new-line in it. Selector = 1.', `{0}This is a text with a + [ + 'This is a text with a\n new-line in it. Selector = 1.', + `{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.`, 1], - ['This is a text with a\n new-line in it. Selector > 1.', `{0}This is a text with a + new-line in it. Selector > 1.`, + 1, + ], + [ + 'This is a text with a\n new-line in it. Selector > 1.', + `{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.`, 5], + new-line in it. Selector > 1.`, + 5, + ], // with double-quotes and id split accros lines - [`This is a text with a - new-line in it. Selector = 1.`, `{0}This is a text with a + [ + `This is a text with a + new-line in it. Selector = 1.`, + `{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.`, 1], + new-line in it. Selector > 1.`, + 1, + ], // with single-quotes and id split accros lines - [`This is a text with a - new-line in it. Selector > 1.`, `{0}This is a text with a + [ + `This is a text with a + new-line in it. Selector > 1.`, + `{0}This is a text with a new-line in it. Selector = 0.|{1}This is a text with a new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.`, 5], + new-line in it. Selector > 1.`, + 5, + ], // with single-quotes and \n in text - ['This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0], + [ + 'This is a text with a\nnew-line in it. Selector = 0.', + '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', + 0, + ], // with double-quotes and id split accros lines - ['This is a text with a\nnew-line in it. Selector = 1.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 1], + [ + 'This is a text with a\nnew-line in it. Selector = 1.', + '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', + 1, + ], // esacape pipe - ['This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0], + [ + 'This is a text with | in it. Selector = 0.', + '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', + 0, + ], // Empty plural set (2 plural forms) from a .PO file ['', '|', 1], // Empty plural set (3 plural forms) from a .PO file @@ -142,6 +208,6 @@ describe('Formatter', () => { ['-2 degrees', '%count% degree|%count% degrees', -2], ['-2 degrés', '%count% degré|%count% degrés', -2], ])('#%# test choose', (expected, id, number, locale = 'en') => { - expect(format(id, {'%count%': number}, locale)).toBe(expected); - }) -}); \ No newline at end of file + expect(format(id, { '%count%': number }, locale)).toBe(expected); + }); +}); diff --git a/src/Translator/assets/test/formatters/intl-formatter.test.ts b/src/Translator/assets/test/formatters/intl-formatter.test.ts index 3d00e7e50ff..cf995772102 100644 --- a/src/Translator/assets/test/formatters/intl-formatter.test.ts +++ b/src/Translator/assets/test/formatters/intl-formatter.test.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import {formatIntl} from '../../src/formatters/intl-formatter'; +import { formatIntl } from '../../src/formatters/intl-formatter'; describe('Intl Formatter', () => { test('format with named arguments', () => { @@ -29,20 +29,24 @@ describe('Intl Formatter', () => { =2 {{host} invites {guest} and one other person to their party.} other {{host} invites {guest} as one of the # people invited to their party.}}}}`.trim(); - const message = formatIntl(chooseMessage, { - gender_of_host: 'male', - num_guests: 10, - host: 'Fabien', - guest: 'Guilherme', - }, 'en'); + const message = formatIntl( + chooseMessage, + { + gender_of_host: 'male', + num_guests: 10, + host: 'Fabien', + guest: 'Guilherme', + }, + 'en' + ); expect(message).toEqual('Fabien invites Guilherme as one of the 9 people invited to his party.'); - }) + }); test('percents and brackets are trimmed', () => { - expect(formatIntl('Hello {name}', { name: 'Fab'}, 'en')).toEqual('Hello Fab'); - expect(formatIntl('Hello {name}', { '%name%': 'Fab'}, 'en')).toEqual('Hello Fab'); - expect(formatIntl('Hello {name}', { '{{ name }}': 'Fab'}, 'en')).toEqual('Hello Fab'); + expect(formatIntl('Hello {name}', { name: 'Fab' }, 'en')).toEqual('Hello Fab'); + expect(formatIntl('Hello {name}', { '%name%': 'Fab' }, 'en')).toEqual('Hello Fab'); + expect(formatIntl('Hello {name}', { '{{ name }}': 'Fab' }, 'en')).toEqual('Hello Fab'); // Parameters object should not be modified const parameters = { '%name%': 'Fab' }; @@ -51,12 +55,12 @@ describe('Intl Formatter', () => { }); test('format with HTML inside', () => { - expect(formatIntl('Hello {name}', { name: 'Fab'}, 'en')).toEqual('Hello Fab'); - expect(formatIntl('Hello {name}', { name: 'Fab'}, 'en')).toEqual('Hello Fab'); - }) + expect(formatIntl('Hello {name}', { name: 'Fab' }, 'en')).toEqual('Hello Fab'); + expect(formatIntl('Hello {name}', { name: 'Fab' }, 'en')).toEqual('Hello Fab'); + }); test('format with locale containg underscore', () => { - expect(formatIntl('Hello {name}', { name: 'Fab'}, 'en_US')).toEqual('Hello Fab'); - expect(formatIntl('Bonjour {name}', { name: 'Fab'}, 'fr_FR')).toEqual('Bonjour Fab'); + expect(formatIntl('Hello {name}', { name: 'Fab' }, 'en_US')).toEqual('Hello Fab'); + expect(formatIntl('Bonjour {name}', { name: 'Fab' }, 'fr_FR')).toEqual('Bonjour Fab'); }); -}); \ No newline at end of file +}); diff --git a/src/Translator/assets/test/translator.test.ts b/src/Translator/assets/test/translator.test.ts index 80ca3f5034a..5a468044cc7 100644 --- a/src/Translator/assets/test/translator.test.ts +++ b/src/Translator/assets/test/translator.test.ts @@ -1,12 +1,19 @@ -import {getLocale, type Message, type NoParametersType, setLocale, setLocaleFallbacks, trans} from '../src/translator'; +import { + getLocale, + type Message, + type NoParametersType, + setLocale, + setLocaleFallbacks, + trans, +} from '../src/translator'; describe('Translator', () => { beforeEach(() => { setLocale(null); - setLocaleFallbacks({}) + setLocaleFallbacks({}); document.documentElement.lang = ''; document.documentElement.removeAttribute('data-symfony-ux-translator-locale'); - }) + }); describe('getLocale', () => { test('default locale', () => { @@ -18,7 +25,7 @@ describe('Translator', () => { expect(getLocale()).toEqual('fr'); // or the locale from , if exists - document.documentElement.setAttribute('data-symfony-ux-translator-locale', 'it') + document.documentElement.setAttribute('data-symfony-ux-translator-locale', 'it'); expect(getLocale()).toEqual('it'); setLocale('de'); @@ -41,17 +48,17 @@ describe('Translator', () => { translations: { messages: { en: 'A basic message', - } - } + }, + }, }; - expect(trans(MESSAGE_BASIC)).toEqual('A basic message') - expect(trans(MESSAGE_BASIC, {})).toEqual('A basic message') - expect(trans(MESSAGE_BASIC, {}, 'messages')).toEqual('A basic message') - expect(trans(MESSAGE_BASIC, {}, 'messages', 'en')).toEqual('A basic message') + expect(trans(MESSAGE_BASIC)).toEqual('A basic message'); + expect(trans(MESSAGE_BASIC, {})).toEqual('A basic message'); + expect(trans(MESSAGE_BASIC, {}, 'messages')).toEqual('A basic message'); + expect(trans(MESSAGE_BASIC, {}, 'messages', 'en')).toEqual('A basic message'); // @ts-expect-error "%count%" is not a valid parameter - expect(trans(MESSAGE_BASIC, {'%count%': 1})).toEqual('A basic message') + expect(trans(MESSAGE_BASIC, { '%count%': 1 })).toEqual('A basic message'); // @ts-expect-error "foo" is not a valid domain expect(trans(MESSAGE_BASIC, {}, 'foo')).toEqual('message.basic'); @@ -61,47 +68,80 @@ describe('Translator', () => { }); test('basic message with parameters', () => { - const MESSAGE_BASIC_WITH_PARAMETERS: Message<{ messages: { parameters: { '%parameter1%': string, '%parameter2%': string } } }, 'en'> = { + const MESSAGE_BASIC_WITH_PARAMETERS: Message< + { messages: { parameters: { '%parameter1%': string; '%parameter2%': string } } }, + 'en' + > = { id: 'message.basic.with.parameters', translations: { messages: { en: 'A basic message %parameter1% %parameter2%', - } - } + }, + }, }; - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { - '%parameter1%': 'foo', - '%parameter2%': 'bar' - })).toEqual('A basic message foo bar'); + expect( + trans(MESSAGE_BASIC_WITH_PARAMETERS, { + '%parameter1%': 'foo', + '%parameter2%': 'bar', + }) + ).toEqual('A basic message foo bar'); - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { - '%parameter1%': 'foo', - '%parameter2%': 'bar' - }, 'messages')).toEqual('A basic message foo bar'); + expect( + trans( + MESSAGE_BASIC_WITH_PARAMETERS, + { + '%parameter1%': 'foo', + '%parameter2%': 'bar', + }, + 'messages' + ) + ).toEqual('A basic message foo bar'); - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { - '%parameter1%': 'foo', - '%parameter2%': 'bar' - }, 'messages', 'en')).toEqual('A basic message foo bar'); + expect( + trans( + MESSAGE_BASIC_WITH_PARAMETERS, + { + '%parameter1%': 'foo', + '%parameter2%': 'bar', + }, + 'messages', + 'en' + ) + ).toEqual('A basic message foo bar'); // @ts-expect-error Parameters "%parameter1%" and "%parameter2%" are missing expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, {})).toEqual('A basic message %parameter1% %parameter2%'); // @ts-expect-error Parameter "%parameter2%" is missing - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, {'%parameter1%': 'foo'})).toEqual('A basic message foo %parameter2%'); - - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { - '%parameter1%': 'foo', - '%parameter2%': 'bar' - // @ts-expect-error "foobar" is not a valid domain - }, 'foobar')).toEqual('message.basic.with.parameters'); - - expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { - '%parameter1%': 'foo', - '%parameter2%': 'bar' - // @ts-expect-error "fr" is not a valid locale - }, 'messages', 'fr')).toEqual('message.basic.with.parameters'); + expect(trans(MESSAGE_BASIC_WITH_PARAMETERS, { '%parameter1%': 'foo' })).toEqual( + 'A basic message foo %parameter2%' + ); + + expect( + trans( + MESSAGE_BASIC_WITH_PARAMETERS, + { + '%parameter1%': 'foo', + '%parameter2%': 'bar', + // @ts-expect-error "foobar" is not a valid domain + }, + 'foobar' + ) + ).toEqual('message.basic.with.parameters'); + + expect( + trans( + MESSAGE_BASIC_WITH_PARAMETERS, + { + '%parameter1%': 'foo', + '%parameter2%': 'bar', + // @ts-expect-error "fr" is not a valid locale + }, + 'messages', + 'fr' + ) + ).toEqual('message.basic.with.parameters'); }); test('intl message', () => { @@ -110,7 +150,7 @@ describe('Translator', () => { translations: { 'messages+intl-icu': { en: 'An intl message', - } + }, }, }; @@ -120,7 +160,7 @@ describe('Translator', () => { expect(trans(MESSAGE_INTL, {}, 'messages', 'en')).toEqual('An intl message'); // @ts-expect-error "%count%" is not a valid parameter - expect(trans(MESSAGE_INTL, {'%count%': 1})).toEqual('An intl message'); + expect(trans(MESSAGE_INTL, { '%count%': 1 })).toEqual('An intl message'); // @ts-expect-error "foo" is not a valid domain expect(trans(MESSAGE_INTL, {}, 'foo')).toEqual('message.intl'); @@ -130,16 +170,19 @@ describe('Translator', () => { }); test('intl message with parameters', () => { - const INTL_MESSAGE_WITH_PARAMETERS: Message<{ - 'messages+intl-icu': { - parameters: { - gender_of_host: 'male' | 'female' | string, - num_guests: number, - host: string, - guest: string, - } - } - }, 'en'> = { + const INTL_MESSAGE_WITH_PARAMETERS: Message< + { + 'messages+intl-icu': { + parameters: { + gender_of_host: 'male' | 'female' | string; + num_guests: number; + host: string; + guest: string; + }; + }; + }, + 'en' + > = { id: 'message.intl.with.parameters', translations: { 'messages+intl-icu': { @@ -160,31 +203,45 @@ describe('Translator', () => { =1 {{host} invites {guest} to their party.} =2 {{host} invites {guest} and one other person to their party.} other {{host} invites {guest} as one of the # people invited to their party.}}}}`.trim(), - } + }, }, }; - expect(trans(INTL_MESSAGE_WITH_PARAMETERS, { - gender_of_host: 'male', - num_guests: 123, - host: 'John', - guest: 'Mary', - })).toEqual('John invites Mary as one of the 122 people invited to his party.'); - + expect( + trans(INTL_MESSAGE_WITH_PARAMETERS, { + gender_of_host: 'male', + num_guests: 123, + host: 'John', + guest: 'Mary', + }) + ).toEqual('John invites Mary as one of the 122 people invited to his party.'); - expect(trans(INTL_MESSAGE_WITH_PARAMETERS, { - gender_of_host: 'female', - num_guests: 44, - host: 'Mary', - guest: 'John', - }, 'messages')).toEqual('Mary invites John as one of the 43 people invited to her party.'); + expect( + trans( + INTL_MESSAGE_WITH_PARAMETERS, + { + gender_of_host: 'female', + num_guests: 44, + host: 'Mary', + guest: 'John', + }, + 'messages' + ) + ).toEqual('Mary invites John as one of the 43 people invited to her party.'); - expect(trans(INTL_MESSAGE_WITH_PARAMETERS, { - gender_of_host: 'female', - num_guests: 1, - host: 'Lola', - guest: 'Hugo', - }, 'messages', 'en')).toEqual('Lola invites Hugo to her party.'); + expect( + trans( + INTL_MESSAGE_WITH_PARAMETERS, + { + gender_of_host: 'female', + num_guests: 1, + host: 'Lola', + guest: 'Hugo', + }, + 'messages', + 'en' + ) + ).toEqual('Lola invites Hugo to her party.'); expect(() => { // @ts-expect-error Parameters "gender_of_host", "num_guests", "host", and "guest" are missing @@ -203,7 +260,7 @@ describe('Translator', () => { trans(INTL_MESSAGE_WITH_PARAMETERS, { gender_of_host: 'male', num_guests: 123, - }) + }); }).toThrow(/^The intl string context variable "host" was not provided/); expect(() => { @@ -212,11 +269,13 @@ describe('Translator', () => { gender_of_host: 'male', num_guests: 123, host: 'John', - }) + }); }).toThrow(/^The intl string context variable "guest" was not provided/); expect( - trans(INTL_MESSAGE_WITH_PARAMETERS, { + trans( + INTL_MESSAGE_WITH_PARAMETERS, + { gender_of_host: 'male', num_guests: 123, host: 'John', @@ -224,10 +283,13 @@ describe('Translator', () => { }, // @ts-expect-error Domain "foobar" is invalid 'foobar' - )).toEqual('message.intl.with.parameters'); + ) + ).toEqual('message.intl.with.parameters'); expect( - trans(INTL_MESSAGE_WITH_PARAMETERS, { + trans( + INTL_MESSAGE_WITH_PARAMETERS, + { gender_of_host: 'male', num_guests: 123, host: 'John', @@ -236,11 +298,15 @@ describe('Translator', () => { 'messages', // @ts-expect-error Locale "fr" is invalid 'fr' - )).toEqual('message.intl.with.parameters'); + ) + ).toEqual('message.intl.with.parameters'); }); test('same message id for multiple domains', () => { - const MESSAGE_MULTI_DOMAINS: Message<{ foobar: { parameters: NoParametersType }, messages: { parameters: NoParametersType } }, 'en'> = { + const MESSAGE_MULTI_DOMAINS: Message< + { foobar: { parameters: NoParametersType }; messages: { parameters: NoParametersType } }, + 'en' + > = { id: 'message.multi_domains', translations: { foobar: { @@ -248,8 +314,8 @@ describe('Translator', () => { }, messages: { en: 'A message from messages catalogue', - } - } + }, + }, }; expect(trans(MESSAGE_MULTI_DOMAINS)).toEqual('A message from messages catalogue'); @@ -271,7 +337,13 @@ describe('Translator', () => { }); test('same message id for multiple domains, and different parameters', () => { - const MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS: Message<{ foobar: { parameters: { '%parameter2%': string } }, messages: { parameters: { '%parameter1%': string } } }, 'en'> = { + const MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS: Message< + { + foobar: { parameters: { '%parameter2%': string } }; + messages: { parameters: { '%parameter1%': string } }; + }, + 'en' + > = { id: 'message.multi_domains.different_parameters', translations: { foobar: { @@ -279,28 +351,47 @@ describe('Translator', () => { }, messages: { en: 'A message from messages catalogue with a parameter %parameter1%', - } - } + }, + }, }; - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter1%': 'foo'})).toEqual('A message from messages catalogue with a parameter foo'); - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter1%': 'foo'}, 'messages')).toEqual('A message from messages catalogue with a parameter foo'); - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter1%': 'foo'}, 'messages', 'en')).toEqual('A message from messages catalogue with a parameter foo'); - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter2%': 'foo'}, 'foobar')).toEqual('A message from foobar catalogue with a parameter foo'); - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter2%': 'foo'}, 'foobar', 'en')).toEqual('A message from foobar catalogue with a parameter foo'); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' })).toEqual( + 'A message from messages catalogue with a parameter foo' + ); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages')).toEqual( + 'A message from messages catalogue with a parameter foo' + ); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages', 'en')).toEqual( + 'A message from messages catalogue with a parameter foo' + ); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter2%': 'foo' }, 'foobar')).toEqual( + 'A message from foobar catalogue with a parameter foo' + ); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter2%': 'foo' }, 'foobar', 'en')).toEqual( + 'A message from foobar catalogue with a parameter foo' + ); // @ts-expect-error Parameter "%parameter1%" is missing - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {})).toEqual('A message from messages catalogue with a parameter %parameter1%'); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {})).toEqual( + 'A message from messages catalogue with a parameter %parameter1%' + ); // @ts-expect-error Domain "baz" is invalid - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter1%': 'foo'}, 'baz')).toEqual('message.multi_domains.different_parameters'); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'baz')).toEqual( + 'message.multi_domains.different_parameters' + ); // @ts-expect-error Locale "fr" is invalid - expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, {'%parameter1%': 'foo'}, 'messages', 'fr')).toEqual('message.multi_domains.different_parameters'); + expect(trans(MESSAGE_MULTI_DOMAINS_WITH_PARAMETERS, { '%parameter1%': 'foo' }, 'messages', 'fr')).toEqual( + 'message.multi_domains.different_parameters' + ); }); test('message from intl domain should be prioritized over its non-intl equivalent', () => { - const MESSAGE: Message<{ 'messages+intl-icu': { parameters: NoParametersType }, messages: { parameters: NoParametersType } }, 'en'> = { + const MESSAGE: Message< + { 'messages+intl-icu': { parameters: NoParametersType }; messages: { parameters: NoParametersType } }, + 'en' + > = { id: 'message', translations: { 'messages+intl-icu': { @@ -308,9 +399,9 @@ describe('Translator', () => { }, messages: { en: 'A basic message', - } - } - } + }, + }, + }; expect(trans(MESSAGE)).toEqual('A intl message'); expect(trans(MESSAGE, {})).toEqual('A intl message'); @@ -319,38 +410,38 @@ describe('Translator', () => { }); test('fallback behavior', () => { - setLocaleFallbacks({fr_FR:'fr',fr:'en',en_US:'en',en_GB:'en',de_DE:'de',de:'en'}); + setLocaleFallbacks({ fr_FR: 'fr', fr: 'en', en_US: 'en', en_GB: 'en', de_DE: 'de', de: 'en' }); - const MESSAGE: Message<{ messages: { parameters: NoParametersType } }, 'en'|'en_US'|'fr'> = { + const MESSAGE: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'> = { id: 'message', translations: { messages: { en: 'A message in english', en_US: 'A message in english (US)', fr: 'Un message en français', - } - } - } + }, + }, + }; - const MESSAGE_INTL: Message<{ messages: { parameters: NoParametersType } }, 'en'|'en_US'|'fr'> = { + const MESSAGE_INTL: Message<{ messages: { parameters: NoParametersType } }, 'en' | 'en_US' | 'fr'> = { id: 'message_intl', translations: { messages: { en: 'A intl message in english', en_US: 'A intl message in english (US)', fr: 'Un message intl en français', - } - } - } + }, + }, + }; const MESSAGE_FRENCH_ONLY: Message<{ messages: { parameters: NoParametersType } }, 'fr'> = { id: 'message_french_only', translations: { messages: { fr: 'Un message en français uniquement', - } - } - } + }, + }, + }; expect(trans(MESSAGE, {}, 'messages', 'en')).toEqual('A message in english'); expect(trans(MESSAGE_INTL, {}, 'messages', 'en')).toEqual('A intl message in english'); @@ -369,6 +460,6 @@ describe('Translator', () => { expect(trans(MESSAGE_FRENCH_ONLY, {}, 'messages', 'fr')).toEqual('Un message en français uniquement'); expect(trans(MESSAGE_FRENCH_ONLY, {}, 'messages', 'en' as 'fr')).toEqual('message_french_only'); - }) + }); }); }); diff --git a/src/Translator/assets/test/utils.test.ts b/src/Translator/assets/test/utils.test.ts index 9a5edb4e9e9..3b0f05977d5 100644 --- a/src/Translator/assets/test/utils.test.ts +++ b/src/Translator/assets/test/utils.test.ts @@ -1,35 +1,43 @@ -import {strtr} from '../src/utils'; +import { strtr } from '../src/utils'; describe('Utils', () => { test.concurrent.each<[string, string, Record]>([ // https://github.com/php/php-src/blob/master/ext/standard/tests/strings/strtr_basic.phpt - ['TEST STrTr', 'test strtr', {t: 'T', e: 'E', st: 'ST'}], - ['TEST STrTr', 'test strtr', {t: 'T', e: 'E', st: 'ST'}], + ['TEST STrTr', 'test strtr', { t: 'T', e: 'E', st: 'ST' }], + ['TEST STrTr', 'test strtr', { t: 'T', e: 'E', st: 'ST' }], // https://github.com/php/php-src/blob/master/ext/standard/tests/strings/strtr_variation1.phpt - ['a23', '123', {'1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b'}], - ['1bc', 'abc', {'1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b'}], - ['a1b2c3', '1a2b3c', {'1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b'}], - [` + ['a23', '123', { '1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b' }], + ['1bc', 'abc', { '1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b' }], + ['a1b2c3', '1a2b3c', { '1': 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b' }], + [ + ` a23 1bc -a1b2c3`, ` +a1b2c3`, + ` 123 abc -1a2b3c`, {1: 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b'}], +1a2b3c`, + { 1: 'a', a: '1', '2b3c': 'b2c3', b2c3: '3c2b' }, + ], // https://github.com/php/php-src/blob/master/ext/standard/tests/strings/strtr_variation2.phpt - ['$', '%', {$: '%', '%': '$', '#*&@()': '()@&*#'}], - ['#%*', '#$*', {$: '%', '%': '$', '#*&@()': '()@&*#'}], - ['text & @()', 'text & @()', {$: '%', '%': '$', '#*&@()': '()@&*#'}], - [` + ['$', '%', { $: '%', '%': '$', '#*&@()': '()@&*#' }], + ['#%*', '#$*', { $: '%', '%': '$', '#*&@()': '()@&*#' }], + ['text & @()', 'text & @()', { $: '%', '%': '$', '#*&@()': '()@&*#' }], + [ + ` $ #%*& -text & @()`, ` +text & @()`, + ` % #$*& -text & @()`, {$: '%', '%': '$', '#*&@()': '()@&*#'}], +text & @()`, + { $: '%', '%': '$', '#*&@()': '()@&*#' }, + ], ])('strtr', (expected, string, replacePairs) => { expect(strtr(string, replacePairs)).toEqual(expected); }); -}); \ No newline at end of file +}); diff --git a/src/Vue/assets/test/register_controller.test.ts b/src/Vue/assets/test/register_controller.test.ts index db4b9ffee36..9b2ed03e4f2 100644 --- a/src/Vue/assets/test/register_controller.test.ts +++ b/src/Vue/assets/test/register_controller.test.ts @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -import {registerVueControllerComponents} from '../src/register_controller'; -import Hello from './fixtures/Hello.vue' -import Goodbye from './fixtures-lazy/Goodbye.vue' +import { registerVueControllerComponents } from '../src/register_controller'; +import Hello from './fixtures/Hello.vue'; +import Goodbye from './fixtures-lazy/Goodbye.vue'; import RequireContext = __WebpackModuleApi.RequireContext; const createFakeFixturesContext = (lazyDir: boolean): RequireContext => { @@ -29,7 +29,6 @@ const createFakeFixturesContext = (lazyDir: boolean): RequireContext => { return context; }; - describe('registerVueControllerComponents', () => { it('should resolve components synchronously', () => { registerVueControllerComponents(createFakeFixturesContext(false)); @@ -51,6 +50,8 @@ describe('registerVueControllerComponents', () => { registerVueControllerComponents(createFakeFixturesContext(false)); const resolveComponent = window.resolveVueComponent; - expect(() => resolveComponent('Helloooo')).toThrow('Vue controller "Helloooo" does not exist. Possible values: Hello'); + expect(() => resolveComponent('Helloooo')).toThrow( + 'Vue controller "Helloooo" does not exist. Possible values: Hello' + ); }); }); diff --git a/src/Vue/assets/test/render_controller.test.ts b/src/Vue/assets/test/render_controller.test.ts index 876bb9e1e05..58112b2338d 100644 --- a/src/Vue/assets/test/render_controller.test.ts +++ b/src/Vue/assets/test/render_controller.test.ts @@ -32,8 +32,8 @@ const startStimulus = () => { }; const Hello = { - template: '

Hello {{ name ?? \'world\' }}

', - props: ['name'] + template: "

Hello {{ name ?? 'world' }}

", + props: ['name'], }; window.resolveVueComponent = () => { diff --git a/yarn.lock b/yarn.lock index 81e45806896..6089543f567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1933,59 +1933,59 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@biomejs/biome@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.7.3.tgz#847a317b63c811534fc8108389b7a9fae8803eed" - integrity sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ== +"@biomejs/biome@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.3.tgz#3b5eecea90d973f71618aae3e6e8be4d2ca23e42" + integrity sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w== optionalDependencies: - "@biomejs/cli-darwin-arm64" "1.7.3" - "@biomejs/cli-darwin-x64" "1.7.3" - "@biomejs/cli-linux-arm64" "1.7.3" - "@biomejs/cli-linux-arm64-musl" "1.7.3" - "@biomejs/cli-linux-x64" "1.7.3" - "@biomejs/cli-linux-x64-musl" "1.7.3" - "@biomejs/cli-win32-arm64" "1.7.3" - "@biomejs/cli-win32-x64" "1.7.3" - -"@biomejs/cli-darwin-arm64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.3.tgz#0b0f568f6fd2153aa1a53bddd0a55355df381952" - integrity sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ== - -"@biomejs/cli-darwin-x64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.3.tgz#0eb0e9da1f869e65e6ce98a007a3341bb1c88446" - integrity sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg== - -"@biomejs/cli-linux-arm64-musl@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.3.tgz#e098110cc11552857ba46cae1e00e68805c1718a" - integrity sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw== - -"@biomejs/cli-linux-arm64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.3.tgz#9e53c14acd7190ebd1e8b0a2f5a54083c118ce72" - integrity sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w== - -"@biomejs/cli-linux-x64-musl@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.3.tgz#f27d4a267f69e663797e3204e51a373f3e33bc30" - integrity sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA== - -"@biomejs/cli-linux-x64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.3.tgz#7112b22e32a8626d0f11d92e43d0cc034c50723d" - integrity sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA== - -"@biomejs/cli-win32-arm64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.3.tgz#91bed8a3ae3433c3feb6a11816b0eb19b60801ef" - integrity sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q== - -"@biomejs/cli-win32-x64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.3.tgz#41b65a940a360abb4a3205949370153ffe30c7de" - integrity sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w== + "@biomejs/cli-darwin-arm64" "1.8.3" + "@biomejs/cli-darwin-x64" "1.8.3" + "@biomejs/cli-linux-arm64" "1.8.3" + "@biomejs/cli-linux-arm64-musl" "1.8.3" + "@biomejs/cli-linux-x64" "1.8.3" + "@biomejs/cli-linux-x64-musl" "1.8.3" + "@biomejs/cli-win32-arm64" "1.8.3" + "@biomejs/cli-win32-x64" "1.8.3" + +"@biomejs/cli-darwin-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz#be2bfdd445cd2d3cb0ff41a96a72ec761753997c" + integrity sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A== + +"@biomejs/cli-darwin-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz#47d408edd9f5c04069fbcf8610bacf1db8c6c0d9" + integrity sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw== + +"@biomejs/cli-linux-arm64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz#44df284383d57cf4f28daeedd080dad7be05df78" + integrity sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ== + +"@biomejs/cli-linux-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz#6a6b1da1dfce0294a028cbb5d6c40d73691dd713" + integrity sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw== + +"@biomejs/cli-linux-x64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz#ceef30a8ee1a00d4ad31e32dd31ba2a661f2719d" + integrity sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA== + +"@biomejs/cli-linux-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz#665df74d19fb8f83001a9d80824d3a1723e2123f" + integrity sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw== + +"@biomejs/cli-win32-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz#0fb6f58990f4de0331a6ed22c47c66f5a89133cc" + integrity sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ== + +"@biomejs/cli-win32-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz#6a9dc5a4e13357277da43c015cd5cdc374035448" + integrity sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg== "@cnakazawa/watch@^1.0.3": version "1.0.4"