Merge remote-tracking branch 'origin/valentin/a11y-refactorings' into valentin/add-vitest-3-support

This commit is contained in:
Valentin Palkovic 2025-01-10 14:36:02 +01:00
commit e6ec8b17ee
9 changed files with 328 additions and 282 deletions

View File

@ -68,6 +68,7 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"!dist/dummy.*",
"README.md",
"*.mjs",
"*.js",
@ -99,7 +100,7 @@
"ansi-to-html": "^0.7.2",
"boxen": "^8.0.1",
"es-toolkit": "^1.22.0",
"execa": "^9.5.2",
"execa": "^8.0.1",
"find-up": "^7.0.0",
"formik": "^2.2.9",
"istanbul-lib-report": "^3.0.1",
@ -151,11 +152,41 @@
],
"nodeEntries": [
"./src/preset.ts",
"./src/vitest-plugin/index.ts",
"./src/vitest-plugin/global-setup.ts",
"./src/postinstall.ts",
"./src/node/vitest.ts",
"./src/node/coverage-reporter.ts"
{
"file": "./src/dummy.ts",
"formats": [
"esm"
]
},
{
"file": "./src/node/vitest.ts",
"formats": [
"esm",
"cjs"
]
},
{
"file": "./src/node/coverage-reporter.ts",
"formats": [
"esm",
"cjs"
]
},
{
"file": "./src/vitest-plugin/index.ts",
"formats": [
"cjs",
"esm"
]
},
{
"file": "./src/vitest-plugin/global-setup.ts",
"formats": [
"cjs",
"esm"
]
}
]
},
"storybook": {

View File

@ -1,4 +1,12 @@
import React, { type ComponentProps, type FC, useCallback, useMemo, useRef, useState } from 'react';
import React, {
type ComponentProps,
type FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Button,
@ -132,18 +140,7 @@ export const TestProviderRender: FC<TestProviderRenderProps> = ({
return false;
}
const allTags = Object.values(internalIndex.entries).reduce((acc, entry) => {
entry.tags?.forEach((tag: Tag) => {
acc.add(tag);
});
return acc;
}, new Set<Tag>());
if (allTags.has('a11y-test')) {
return true;
}
return false;
return Object.values(internalIndex.entries).some((entry) => entry.tags?.includes('a11y-test'));
}, [isA11yAddon, storybookState.internal_index]);
const [config, updateConfig] = useConfig(
@ -460,10 +457,7 @@ function useConfig(api: API, providerId: string, initialConfig: Config) {
[api, providerId]
);
const [currentConfig, setConfig] = useState<Config>(() => {
updateTestProviderState(initialConfig);
return initialConfig;
});
const [currentConfig, setConfig] = useState<Config>(initialConfig);
const lastConfig = useRef(initialConfig);
@ -488,5 +482,9 @@ function useConfig(api: API, providerId: string, initialConfig: Config) {
[saveConfig]
);
useEffect(() => {
updateTestProviderState(initialConfig);
}, []);
return [currentConfig, updateConfig] as const;
}

View File

@ -0,0 +1,4 @@
// Due to a bug in tsup, this file is necessary because otherwise if for a certain build step a root file isn't given
// tsup will flatten the directory structure and the output will be in the root of the dist output directory
// https://github.com/egoist/tsup/issues/728
export default {};

View File

@ -1,200 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`addonA11yAddonTest > prompt > should return auto prompt if transformedPreviewCode is defined and if transformedSetupCode is skipped 1`] = `
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/preview.js file to set up tags from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`;
exports[`addonA11yAddonTest > prompt > should return auto prompt if transformedSetupCode is defined and if transformedPreviewCode is null 1`] = `
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/vitest.setup.ts file to set up project annotations from @storybook/addon-a11y.
2) We couldn't find or automatically update your .storybook/preview.<ts|js> in your project to smoothly set up tags from @storybook/addon-a11y.
Please manually update your .storybook/preview.<ts|js> file to include the following:
export default {
...
+ tags: ["a11y-test"],
}
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`;
exports[`addonA11yAddonTest > prompt > should return auto prompt if transformedSetupCode is defined and if transformedPreviewCode is skipped 1`] = `
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/vitest.setup.ts file to set up project annotations from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`;
exports[`addonA11yAddonTest > prompt > should return auto prompt if transformedSetupCode is null and if transformedPreviewCode is defined 1`] = `
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We couldn't find or automatically update your .storybook/vitest.setup.<ts|js> in your project to smoothly set up project annotations from @storybook/addon-a11y.
Please manually update your vitest.setup.ts file to include the following:
...
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
const annotations = setProjectAnnotations([
...
+ a11yAddonAnnotations,
]);
beforeAll(annotations.beforeAll);
2) We have to update your .storybook/preview.js file to set up tags from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`;
exports[`addonA11yAddonTest > prompt > should return manual prompt if transformedSetupCode is null and if transformedPreviewCode is null 1`] = `
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We couldn't find or automatically update your .storybook/vitest.setup.<ts|js> in your project to smoothly set up project annotations from @storybook/addon-a11y.
Please manually update your vitest.setup.ts file to include the following:
...
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
const annotations = setProjectAnnotations([
...
+ a11yAddonAnnotations,
]);
beforeAll(annotations.beforeAll);
2) We couldn't find or automatically update your .storybook/preview.<ts|js> in your project to smoothly set up tags from @storybook/addon-a11y.
Please manually update your .storybook/preview.<ts|js> file to include the following:
export default {
...
+ tags: ["a11y-test"],
}
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should add a new tags property if it does not exist 1`] = `
"import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: [/*'a11y-test'*/]
};
export default preview;"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should add a new tags property if it does not exist and a default export does not exist 1`] = `
"export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
}
export const tags = ["a11y-test"];"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should extend the existing tags property 1`] = `
"import type { Preview } from "@storybook/react";
const preview: Preview = {
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: ["existingTag"/*, "a11y-test"*/],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should extend the existing tags property without type annotations 1`] = `
"export default {
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: ["existingTag"/*, "a11y-test"*/],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should handle the default export without type annotations 1`] = `
"export default {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: [/*"a11y-test"*/]
};"
`;
exports[`addonA11yAddonTest > transformPreviewFile > should not add a11y-test if it already exists in the tags property 1`] = `
"import type { Preview } from "@storybook/react";
const preview: Preview = {
tags: ["a11y-test"],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;"
`;

View File

@ -366,7 +366,35 @@ describe('addonA11yAddonTest', () => {
skipPreviewTransformation: false,
skipVitestSetupTransformation: false,
});
expect(result).toMatchSnapshot();
expect(result).toMatchInlineSnapshot(`
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We couldn't find or automatically update your .storybook/vitest.setup.<ts|js> in your project to smoothly set up project annotations from @storybook/addon-a11y.
Please manually update your vitest.setup.ts file to include the following:
...
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
const annotations = setProjectAnnotations([
...
+ a11yAddonAnnotations,
]);
beforeAll(annotations.beforeAll);
2) We couldn't find or automatically update your .storybook/preview.<ts|js> in your project to smoothly set up tags from @storybook/addon-a11y.
Please manually update your .storybook/preview.<ts|js> file to include the following:
export default {
...
+ tags: ["a11y-test"],
}
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`);
});
it('should return auto prompt if transformedSetupCode is null and if transformedPreviewCode is defined', () => {
@ -378,7 +406,29 @@ describe('addonA11yAddonTest', () => {
skipPreviewTransformation: false,
skipVitestSetupTransformation: false,
});
expect(result).toMatchSnapshot();
expect(result).toMatchInlineSnapshot(`
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We couldn't find or automatically update your .storybook/vitest.setup.<ts|js> in your project to smoothly set up project annotations from @storybook/addon-a11y.
Please manually update your vitest.setup.ts file to include the following:
...
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
const annotations = setProjectAnnotations([
...
+ a11yAddonAnnotations,
]);
beforeAll(annotations.beforeAll);
2) We have to update your .storybook/preview.js file to set up tags from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`);
});
it('should return auto prompt if transformedSetupCode is defined and if transformedPreviewCode is null', () => {
@ -390,7 +440,24 @@ describe('addonA11yAddonTest', () => {
skipPreviewTransformation: false,
skipVitestSetupTransformation: false,
});
expect(result).toMatchSnapshot();
expect(result).toMatchInlineSnapshot(`
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/vitest.setup.ts file to set up project annotations from @storybook/addon-a11y.
2) We couldn't find or automatically update your .storybook/preview.<ts|js> in your project to smoothly set up tags from @storybook/addon-a11y.
Please manually update your .storybook/preview.<ts|js> file to include the following:
export default {
...
+ tags: ["a11y-test"],
}
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`);
});
it('should return auto prompt if transformedSetupCode is defined and if transformedPreviewCode is skipped', () => {
@ -402,7 +469,16 @@ describe('addonA11yAddonTest', () => {
skipPreviewTransformation: true,
skipVitestSetupTransformation: false,
});
expect(result).toMatchSnapshot();
expect(result).toMatchInlineSnapshot(`
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/vitest.setup.ts file to set up project annotations from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`);
});
it('should return auto prompt if transformedPreviewCode is defined and if transformedSetupCode is skipped', () => {
@ -414,7 +490,16 @@ describe('addonA11yAddonTest', () => {
skipPreviewTransformation: false,
skipVitestSetupTransformation: true,
});
expect(result).toMatchSnapshot();
expect(result).toMatchInlineSnapshot(`
"We have detected that you have @storybook/addon-a11y and @storybook/experimental-addon-test installed.
@storybook/addon-a11y integrates now with @storybook/experimental-addon-test to provide automatic accessibility checks for your stories, powered by Axe and Vitest.
1) We have to update your .storybook/preview.js file to set up tags from @storybook/addon-a11y.
For more information, please refer to the accessibility addon documentation:
https://storybook.js.org/docs/writing-tests/accessibility-testing#test-addon-integration"
`);
});
});
@ -580,7 +665,26 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: [/*'a11y-test'*/]
};
export default preview;"
`);
});
it('should add a new tags property if it does not exist and a default export does not exist', async () => {
@ -597,7 +701,17 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"export const parameters = {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
}
export const tags = ["a11y-test"];"
`);
});
it('should extend the existing tags property', async () => {
@ -621,7 +735,25 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"import type { Preview } from "@storybook/react";
const preview: Preview = {
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: ["existingTag"/*, "a11y-test"*/],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;"
`);
});
it('should not add a11y-test if it already exists in the tags property', async () => {
@ -645,7 +777,23 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"import type { Preview } from "@storybook/react";
const preview: Preview = {
tags: ["a11y-test"],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;"
`);
});
it('should handle the default export without type annotations', async () => {
@ -664,7 +812,22 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"export default {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: [/*"a11y-test"*/]
};"
`);
});
it('should extend the existing tags property without type annotations', async () => {
@ -684,7 +847,21 @@ describe('addonA11yAddonTest', () => {
const transformed = await transformPreviewFile(source, process.cwd());
expect(transformed).toMatchSnapshot();
expect(transformed).toMatchInlineSnapshot(`
"export default {
// a11y-test tag controls whether accessibility tests are run as part of a standalone Vitest test run
// For more information please visit: https://storybook.js.org/docs/writing-tests/accessibility-testing
tags: ["existingTag"/*, "a11y-test"*/],
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};"
`);
});
});
});

View File

@ -25,10 +25,6 @@ export const fileExtensions = [
'.mjs',
'.jsx',
'.tsx',
'.ctsx',
'.mtsx',
'.cjsx',
'.mjsx',
] as const;
interface AddonA11yAddonTestOptions {

View File

@ -6478,7 +6478,7 @@ __metadata:
ansi-to-html: "npm:^0.7.2"
boxen: "npm:^8.0.1"
es-toolkit: "npm:^1.22.0"
execa: "npm:^9.5.2"
execa: "npm:^8.0.1"
find-up: "npm:^7.0.0"
formik: "npm:^2.2.9"
istanbul-lib-report: "npm:^3.0.1"

View File

@ -84,7 +84,9 @@ export const addBundlerEntries = async (config: KnipConfig) => {
const bundler = manifest?.bundler;
for (const value of Object.values(bundler ?? {})) {
if (Array.isArray(value)) {
configEntries.push(...value);
configEntries.push(
...value.map((entry) => (typeof entry === 'string' ? entry : entry.file))
);
}
}
config.workspaces[configKey].entry = Array.from(new Set(configEntries));

View File

@ -21,11 +21,19 @@ import { esbuild } from './tools';
/* TYPES */
/**
* A NodeEntry can be a string or an object with a file and format property. If it's a string, it's
* the path to the entry file and will be built for CJS. If it's an object, 'file' is the path to
* the entry file and the 'formats' to build it in. The formats property can be 'esm', 'cjs', or
* both.
*/
type NodeEntry = string | { file: string; formats: Formats[] };
type Formats = 'esm' | 'cjs';
type BundlerConfig = {
previewEntries: string[];
managerEntries: string[];
nodeEntries: string[];
nodeEntries: NodeEntry[];
exportEntries: string[];
externals: string[];
pre: string;
@ -211,34 +219,51 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => {
}
if (nodeEntries.length > 0) {
const { dtsConfig, tsConfigExists } = await getDTSConfigs({
formats: ['esm'],
entries: nodeEntries,
optimized,
});
tasks.push(async () => {
await Promise.all([
build({
...commonOptions,
entry: nodeEntries.map((e: string) => slash(join(cwd, e))),
format: ['cjs'],
target: 'node18',
platform: 'node',
external: commonExternals,
esbuildOptions: (c) => {
c.platform = 'node';
Object.assign(c, getESBuildOptions(optimized));
},
}),
build({
...commonOptions,
...(optimized ? dtsConfig : {}),
entry: nodeEntries.map((e: string) => slash(join(cwd, e))),
format: ['esm'],
target: 'node18',
platform: 'neutral',
banner: {
js: dedent`
const cjsEntries = nodeEntries.filter(
(n) => typeof n === 'string' || n.formats.includes('cjs')
);
const esmEntries = nodeEntries.filter(
(n) => typeof n !== 'string' && n.formats.includes('esm')
) as (NodeEntry & object)[];
const builds = [];
if (cjsEntries.length > 0) {
builds.push(() =>
build({
...commonOptions,
entry: cjsEntries.map((e) => slash(join(cwd, typeof e === 'string' ? e : e.file))),
format: ['cjs'],
target: 'node18',
platform: 'node',
external: commonExternals,
esbuildOptions: (c) => {
c.platform = 'node';
Object.assign(c, getESBuildOptions(optimized));
},
})
);
}
if (esmEntries.length > 0) {
const { dtsConfig, tsConfigExists } = await getDTSConfigs({
formats: ['esm'],
entries: esmEntries.map((e) => e.file),
optimized,
});
builds.push(() =>
build({
...commonOptions,
...(optimized ? dtsConfig : {}),
entry: esmEntries.map((e) => slash(join(cwd, e.file))),
format: ['esm'],
target: 'node18',
platform: 'neutral',
banner: {
js: dedent`
import ESM_COMPAT_Module from "node:module";
import { fileURLToPath as ESM_COMPAT_fileURLToPath } from 'node:url';
import { dirname as ESM_COMPAT_dirname } from 'node:path';
@ -246,24 +271,37 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => {
const __dirname = ESM_COMPAT_dirname(__filename);
const require = ESM_COMPAT_Module.createRequire(import.meta.url);
`,
},
external: [...commonExternals, ...nodeInternals],
esbuildOptions: (c) => {
c.mainFields = ['main', 'module', 'node'];
c.conditions = ['node', 'module', 'import', 'require'];
c.platform = 'neutral';
Object.assign(c, getESBuildOptions(optimized));
},
}),
]);
},
external: [...commonExternals, ...nodeInternals],
esbuildOptions: (c) => {
c.mainFields = ['main', 'module', 'node'];
c.conditions = ['node', 'module', 'import', 'require'];
c.platform = 'neutral';
Object.assign(c, getESBuildOptions(optimized));
},
})
);
if (tsConfigExists && !optimized) {
tasks.push(...esmEntries.map((entry) => () => generateDTSMapperFile(entry.file)));
}
}
await Promise.all(builds.map((b) => b()));
if (!watch) {
await readMetafiles({ formats: ['esm', 'cjs'] });
const metafileFormats: Formats[] = [];
if (cjsEntries.length > 0) {
metafileFormats.push('cjs');
}
if (esmEntries.length > 0) {
metafileFormats.push('esm');
}
await readMetafiles({ formats: metafileFormats });
}
});
if (tsConfigExists && !optimized) {
tasks.push(...nodeEntries.map((entry) => () => generateDTSMapperFile(entry)));
}
}
for (const task of tasks) {