diff --git a/.changeset/three-gorillas-turn.md b/.changeset/three-gorillas-turn.md new file mode 100644 index 0000000..c31acc2 --- /dev/null +++ b/.changeset/three-gorillas-turn.md @@ -0,0 +1,5 @@ +--- +"nx-update-ts-references": patch +--- + +Implement test suite diff --git a/apps/nx-update-ts-references/.c8rc.json b/apps/nx-update-ts-references/.c8rc.json new file mode 100644 index 0000000..ef9f835 --- /dev/null +++ b/apps/nx-update-ts-references/.c8rc.json @@ -0,0 +1,46 @@ +{ + "reporter": [ + "html", + "text-summary" + ], + "exclude": [ + "coverage/**", + "eslint.config.js", + "src/tests/**" + ], + "exclude-after-remap": true, + "extension": [ + ".js", + ".cjs", + ".mjs", + ".ts", + ".cts", + ".mts", + ".tsx", + ".jsx" + ], + + "lines": 100, + "statements": 100, + "functions": 100, + "branches": 100, + + "watermarks": { + "lines": [ + 100, + 95 + ], + "functions": [ + 100, + 95 + ], + "branches": [ + 100, + 95 + ], + "statements": [ + 100, + 95 + ] + } +} diff --git a/apps/nx-update-ts-references/project.json b/apps/nx-update-ts-references/project.json index 34d5332..29fb6e0 100644 --- a/apps/nx-update-ts-references/project.json +++ b/apps/nx-update-ts-references/project.json @@ -7,6 +7,10 @@ "npm-update-ts-references": {}, "tsc": {}, "npm-populate-files": {}, + "coverage-reset": {}, + "mocha-unit-test": {}, + "mocha-integration-test": {}, + "coverage-report": {}, "analyze:_": {}, "analyze:format": {}, "analyze:lint": {}, diff --git a/apps/nx-update-ts-references/src/tests/chai-hooks.ts b/apps/nx-update-ts-references/src/tests/chai-hooks.ts new file mode 100644 index 0000000..acca2a2 --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/chai-hooks.ts @@ -0,0 +1,4 @@ +import { use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +export const { expect } = use(chaiAsPromised); diff --git a/apps/nx-update-ts-references/src/tests/integration/cli.spec.ts b/apps/nx-update-ts-references/src/tests/integration/cli.spec.ts new file mode 100644 index 0000000..3563476 --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/integration/cli.spec.ts @@ -0,0 +1,64 @@ +import { exec } from 'node:child_process'; +import Path from 'node:path'; +import { promisify } from 'node:util'; +import { suite, test } from 'mocha-chain'; +import { expect } from '../chai-hooks.js'; + +const execAsync = promisify(exec); +const projectRoot = Path.join(import.meta.dirname, '../../..'); + +suite('cli', () => { + test('--help', async () => { + const result = await execAsync('./bin.mjs --help'); + + expect(result.stdout).to.contain( + "Write tsconfig.json's references field based on Nx detected dependencies" + ); + expect(result.stderr).to.equal(''); + }); + + test('--version', async () => { + const result = await execAsync('./bin.mjs --version'); + + expect(result.stdout).to.match(/\d+.\d+.\d+/u); + expect(result.stderr).to.equal(''); + }); + + suite('commands', () => { + suite('default/update-ts-references', () => { + test('success', async () => { + const result = await execAsync(`./bin.mjs --project-root ${projectRoot} --ci`); + + expect(result.stdout).to.contain(''); + // https://github.com/nrwl/nx/issues/29244 + expect(result.stderr).to.contain( + 'The inspector is disabled, coverage could not be collected' + ); + const stdErrWithoutWarning = result.stderr + .replace( + /\(node:\d+\) Warning: The inspector is disabled, coverage could not be collected\n/u, + '' + ) + .replace( + /\(Use `pnpm --trace-warnings ...` to show where the warning was created\)\n/u, + '' + ); + expect(stdErrWithoutWarning).to.equal(''); + }); + + test('unknown options', async () => { + await expect(execAsync('./bin.mjs --unknown --option')) + .to.eventually.be.rejectedWith(Error) + .that.has.property('stderr') + .that.contain('Unknown arguments: unknown, option'); + }); + + test('failure', async () => { + const fakeRoot = Path.join(projectRoot, 'does-not-exist'); + await expect( + execAsync(`./bin.mjs --project-root ${fakeRoot} --ci`) + ).to.eventually.be.rejectedWith(Error, `Directory not found: ${fakeRoot}`); + }); + }); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/commands/lib/types.spec.ts b/apps/nx-update-ts-references/src/tests/unit/commands/lib/types.spec.ts new file mode 100644 index 0000000..ae2a3fa --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/commands/lib/types.spec.ts @@ -0,0 +1,7 @@ +import { suite, test } from 'mocha-chain'; + +suite('types', () => { + test('coverage', async () => { + await import('../../../../commands/lib/types.js'); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/commands/update-ts-references-command.spec.ts b/apps/nx-update-ts-references/src/tests/unit/commands/update-ts-references-command.spec.ts new file mode 100644 index 0000000..e6cefa4 --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/commands/update-ts-references-command.spec.ts @@ -0,0 +1,258 @@ +import type { readFile as ReadFile } from 'node:fs/promises'; +import type { ProjectGraph } from '@nx/devkit'; +import type { AsyncSupplier } from 'npm-haywire'; +import { verifyAndRestore } from 'sinon'; +import { afterEach, beforeEach, suite } from 'mocha-chain'; +import { stubMethod } from 'sinon-typed-stub'; +import type { ParseCwd } from '../../../commands/lib/dependencies.js'; +import { UpdateTsReferencesCommand } from '../../../commands/update-ts-references-command.js'; +import type { IUpdateTsReferences } from '../../../lib/update-ts-references.js'; +import { expect } from '../../chai-hooks.js'; + +suite('UpdateTsReferencesCommand', () => { + afterEach(() => { + verifyAndRestore(); + }); + + const withStubs = beforeEach(() => { + const stubbedUpdateTsReferences = stubMethod(); + const stubbedParseCwd = stubMethod(); + const stubbedGetProjectGraph = stubMethod>(); + const stubbedReadFile = stubMethod(); + return { + stubbedUpdateTsReferences: stubbedUpdateTsReferences.stub, + stubbedParseCwd: stubbedParseCwd.stub, + stubbedGetProjectGraph: stubbedGetProjectGraph.stub, + stubbedReadFile: stubbedReadFile.stub, + updateTsReferencesCommand: new UpdateTsReferencesCommand( + stubbedUpdateTsReferences.method, + stubbedParseCwd.method, + stubbedGetProjectGraph.method, + stubbedReadFile.method + ), + }; + }); + + suite('handler', () => { + withStubs.test('success', async ctx => { + ctx.stubbedParseCwd + .withArgs('') + .resolves('/package/root/path/to/projectName'); + + ctx.stubbedGetProjectGraph.resolves({ + dependencies: { + projectName: [ + { + type: '', + target: 'dependency1', + source: '', + }, + { + type: '', + target: 'dependency2', + source: '', + }, + { + type: '', + target: 'dependency3', + source: '', + }, + ], + }, + nodes: { + projectName: { + type: 'app', + name: 'projectName', + data: { + root: 'path/to/projectName', + }, + }, + dependency1: { + type: 'lib', + name: 'dependency1', + data: { + root: 'path/to/dependency-1', + }, + }, + dependency2: { + type: 'lib', + name: 'dependency2', + data: { + root: 'path/to/dependency/2', + }, + }, + }, + }); + + ctx.stubbedReadFile + .withArgs('/package/root/path/to/projectName/project.json', 'utf8') + .resolves( + JSON.stringify({ + name: 'projectName', + }) + ); + + ctx.stubbedUpdateTsReferences.resolves(true); + + await ctx.updateTsReferencesCommand.handler({ + packageRoot: '', + ci: false, + dryRun: true, + }); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + dependencyRootPaths: [ + '/package/root/path/to/dependency-1/tsconfig.json', + '/package/root/path/to/dependency/2/tsconfig.json', + ], + dryRun: true, + tsConfigPath: '/package/root/path/to/projectName/tsconfig.json', + }, + ]); + }); + + withStubs.test('Empty dependencies', async ctx => { + ctx.stubbedParseCwd.resolves('/package/root/path/to/projectName'); + + ctx.stubbedGetProjectGraph.resolves({ + dependencies: { + projectName: [], + }, + nodes: { + projectName: { + type: 'app', + name: 'projectName', + data: { + root: 'path/to/projectName', + }, + }, + }, + }); + + ctx.stubbedReadFile.resolves( + JSON.stringify({ + name: 'projectName', + }) + ); + + ctx.stubbedUpdateTsReferences.resolves(false); + + await ctx.updateTsReferencesCommand.handler({ + packageRoot: '', + ci: false, + dryRun: false, + }); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + dependencyRootPaths: [], + dryRun: false, + tsConfigPath: '/package/root/path/to/projectName/tsconfig.json', + }, + ]); + }); + + withStubs.test('Fails when changed and CI', async ctx => { + ctx.stubbedParseCwd.resolves('/package/root/path/to/projectName'); + + ctx.stubbedGetProjectGraph.resolves({ + dependencies: { + projectName: [ + { + type: '', + target: 'dependency1', + source: '', + }, + ], + }, + nodes: { + projectName: { + type: 'app', + name: 'projectName', + data: { + root: 'path/to/projectName', + }, + }, + dependency1: { + type: 'lib', + name: 'dependency1', + data: { + root: 'path/to/dependency-1', + }, + }, + }, + }); + + ctx.stubbedReadFile.resolves( + JSON.stringify({ + name: 'projectName', + }) + ); + + ctx.stubbedUpdateTsReferences.resolves(true); + + await expect( + ctx.updateTsReferencesCommand.handler({ + packageRoot: '', + ci: true, + dryRun: false, + }) + ).to.eventually.be.rejectedWith(Error, 'tsconfig.json is not built'); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + dependencyRootPaths: ['/package/root/path/to/dependency-1/tsconfig.json'], + dryRun: true, + tsConfigPath: '/package/root/path/to/projectName/tsconfig.json', + }, + ]); + }); + + withStubs.test('Fails when project.json cannot be loaded', async ctx => { + ctx.stubbedParseCwd.resolves('/package/root/path/to/projectName'); + + ctx.stubbedGetProjectGraph.resolves({ + dependencies: { + projectName: [ + { + type: '', + target: 'dependency1', + source: '', + }, + ], + }, + nodes: { + projectName: { + type: 'app', + name: 'projectName', + data: { + root: 'path/to/projectName', + }, + }, + dependency1: { + type: 'lib', + name: 'dependency1', + data: { + root: 'path/to/dependency-1', + }, + }, + }, + }); + + ctx.stubbedReadFile.resolves( + JSON.stringify({ + ignore: true, + }) + ); + + await expect( + ctx.updateTsReferencesCommand.handler({ + packageRoot: '', + ci: false, + dryRun: false, + }) + ).to.eventually.be.rejectedWith(Error, 'Cannot parse project.json name'); + }); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/executor.spec.ts b/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/executor.spec.ts new file mode 100644 index 0000000..7b644bc --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/executor.spec.ts @@ -0,0 +1,204 @@ +import { verifyAndRestore } from 'sinon'; +import { afterEach, beforeEach, suite } from 'mocha-chain'; +import { stubMethod } from 'sinon-typed-stub'; +import { NxUpdateTsReferencesExecutor } from '../../../../executors/update-ts-references/executor.js'; +import type { IUpdateTsReferences } from '../../../../lib/update-ts-references.js'; +import { expect } from '../../../chai-hooks.js'; + +suite('NxUpdateTsReferencesExecutor', () => { + afterEach(() => { + verifyAndRestore(); + }); + + const withStubs = beforeEach(() => { + const stubbedUpdateTsReferences = stubMethod(); + const stubbedLogger = stubMethod(); + return { + stubbedUpdateTsReferences: stubbedUpdateTsReferences.stub, + stubbedLogger: stubbedLogger.stub, + nxUpdateTsReferencesExecutor: new NxUpdateTsReferencesExecutor( + true, + stubbedUpdateTsReferences.method, + stubbedLogger.method + ), + }; + }); + + withStubs.test('success', async ctx => { + ctx.stubbedUpdateTsReferences.resolves(true); + + expect( + await ctx.nxUpdateTsReferencesExecutor.execute( + { check: false, dryRun: false }, + { + projectName: 'projectName', + root: '/path/to/workspace', + projectsConfigurations: { + projects: { + projectName: { + root: '/path/to/package-name', + }, + dependency1: { + root: '/path/to/dependency-1', + }, + dependency2: { + root: '/path/to/dependency/2', + }, + ignore: { + root: '', + }, + }, + version: 123, + }, + projectGraph: { + dependencies: { + projectName: [ + { + type: 'lib', + target: 'dependency1', + source: '', + }, + { + type: 'lib', + target: 'dependency2', + source: '', + }, + ], + dependency1: [ + { + type: 'lib', + target: '', + source: '', + }, + ], + }, + nodes: {}, + }, + nxJsonConfiguration: {}, + cwd: '', + isVerbose: false, + } + ) + ).to.deep.equal({ success: true }); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + tsConfigPath: '/path/to/workspace/path/to/package-name/tsconfig.json', + dependencyRootPaths: [ + '/path/to/workspace/path/to/dependency-1/tsconfig.json', + '/path/to/workspace/path/to/dependency/2/tsconfig.json', + ], + dryRun: false, + }, + ]); + expect(ctx.stubbedLogger.called).to.equal(false); + }); + + withStubs.test('Options are optional', async ctx => { + ctx.stubbedUpdateTsReferences.resolves(false); + + expect( + await ctx.nxUpdateTsReferencesExecutor.execute( + {}, + { + projectName: 'projectName', + root: '/path/to/workspace', + projectsConfigurations: { + projects: { + projectName: { + root: '/path/to/package-name', + }, + dependency1: { + root: '/path/to/dependency-1', + }, + }, + version: 123, + }, + projectGraph: { + dependencies: { + projectName: [ + { + type: 'lib', + target: 'dependency1', + source: '', + }, + { + type: 'lib', + target: 'dependency2', + source: '', + }, + ], + }, + nodes: {}, + }, + nxJsonConfiguration: {}, + cwd: '', + isVerbose: false, + } + ) + ).to.deep.equal({ success: true }); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + tsConfigPath: '/path/to/workspace/path/to/package-name/tsconfig.json', + dependencyRootPaths: ['/path/to/workspace/path/to/dependency-1/tsconfig.json'], + dryRun: true, + }, + ]); + expect(ctx.stubbedLogger.called).to.equal(false); + }); + + withStubs.test('Fails when changes occur during check', async ctx => { + ctx.stubbedUpdateTsReferences.resolves(true); + + expect( + await ctx.nxUpdateTsReferencesExecutor.execute( + { check: true, dryRun: true }, + { + projectName: 'projectName', + root: '/path/to/workspace', + projectsConfigurations: { + projects: { + projectName: { + root: '/path/to/package-name', + }, + dependency1: { + root: '/path/to/dependency-1', + }, + }, + version: 123, + }, + projectGraph: { + dependencies: { + projectName: [ + { + type: 'lib', + target: 'dependency1', + source: '', + }, + { + type: 'lib', + target: 'dependency2', + source: '', + }, + ], + }, + nodes: {}, + }, + nxJsonConfiguration: {}, + cwd: '', + isVerbose: false, + } + ) + ).to.deep.equal({ success: false }); + + expect(ctx.stubbedUpdateTsReferences.getCall(0).args).to.deep.equal([ + { + tsConfigPath: '/path/to/workspace/path/to/package-name/tsconfig.json', + dependencyRootPaths: ['/path/to/workspace/path/to/dependency-1/tsconfig.json'], + dryRun: true, + }, + ]); + expect(ctx.stubbedLogger.getCall(0).args).to.deep.equal(['tsconfig.json is out of date']); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/index.spec.ts b/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/index.spec.ts new file mode 100644 index 0000000..334e60b --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/executors/update-ts-references/index.spec.ts @@ -0,0 +1,7 @@ +import { suite, test } from 'mocha-chain'; + +suite('types', () => { + test('coverage', async () => { + await import('../../../../executors/update-ts-references/index.cjs'); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/file-content.spec.ts b/apps/nx-update-ts-references/src/tests/unit/file-content.spec.ts new file mode 100644 index 0000000..25b728c --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/file-content.spec.ts @@ -0,0 +1,20 @@ +import Path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadAndPopulateFiles } from 'npm-load-populate-files'; +import { suite, test } from 'mocha-chain'; + +const filePath = fileURLToPath(import.meta.url); + +suite('FileContent', () => { + test('Files are populated', async () => { + await loadAndPopulateFiles( + { + filePath: Path.join(filePath, '../../../file-content.js'), + }, + { + check: true, + cwd: Path.join(filePath, '../../../..'), + } + ); + }); +}); diff --git a/apps/nx-update-ts-references/src/tests/unit/lib/update-ts-references.spec.ts b/apps/nx-update-ts-references/src/tests/unit/lib/update-ts-references.spec.ts new file mode 100644 index 0000000..b3766bf --- /dev/null +++ b/apps/nx-update-ts-references/src/tests/unit/lib/update-ts-references.spec.ts @@ -0,0 +1,200 @@ +import type { readFile as ReadFile } from 'node:fs/promises'; +import type { TextFormatter } from 'npm-format-file'; +import type { PopulateFile } from 'npm-populate-files'; +import { verifyAndRestore } from 'sinon'; +import { afterEach, beforeEach, suite } from 'mocha-chain'; +import { stubMethod } from 'sinon-typed-stub'; +import { UpdateTsReferencesFactory } from '../../../lib/update-ts-references.js'; +import { expect } from '../../chai-hooks.js'; + +suite('UpdateTsReferencesFactory', () => { + afterEach(() => { + verifyAndRestore(); + }); + + const withStubs = beforeEach(() => { + const stubbedReadFile = stubMethod(); + const stubbedTextFormatter = stubMethod(); + const stubbedPopulateFile = stubMethod(); + return { + stubbedReadFile: stubbedReadFile.stub, + stubbedTextFormatter: stubbedTextFormatter.stub, + stubbedPopulateFile: stubbedPopulateFile.stub, + updateTsReferencesFactory: new UpdateTsReferencesFactory( + stubbedReadFile.method, + stubbedTextFormatter.method, + stubbedPopulateFile.method + ), + }; + }); + + suite('updateTsReferences', () => { + withStubs.test('success', async ctx => { + ctx.stubbedReadFile.withArgs('/path/to/ts/config/tsconfig.json', 'utf8').resolves( + JSON.stringify( + { + extends: '../../tsconfig.build.json', + compilerOptions: { + outDir: 'dist', + rootDir: 'src', + tsBuildInfoFile: 'dist/tsconfig.tsbuildinfo', + }, + }, + null, + 4 + ) + ); + ctx.stubbedReadFile.withArgs('/path/to/depdendency-1/tsconfig.json', 'utf8').resolves( + JSON.stringify({ + references: [], + }) + ); + ctx.stubbedReadFile.withArgs('/path/to/depdendency-2/tsconfig.json', 'utf8').resolves( + JSON.stringify({ + compilerOptions: {}, + }) + ); + + ctx.stubbedTextFormatter.resolves(''); + + ctx.stubbedPopulateFile + .withArgs( + { + filePath: '/path/to/ts/config/tsconfig.json', + content: '', + }, + { + dryRun: false, + } + ) + .resolves({ updated: true, filePath: '', reason: 'content-changed' }); + + expect( + await ctx.updateTsReferencesFactory.updateTsReferences({ + tsConfigPath: '/path/to/ts/config/tsconfig.json', + dependencyRootPaths: [ + '/path/to/depdendency-1/tsconfig.json', + '/path/to/depdendency-2/tsconfig.json', + ], + dryRun: false, + }) + ).to.equal(true); + + expect(ctx.stubbedReadFile.callCount).to.equal(3); + expect(ctx.stubbedTextFormatter.callCount).to.equal(1); + expect(ctx.stubbedPopulateFile.callCount).to.equal(1); + + expect(ctx.stubbedTextFormatter.getCall(0).args).to.deep.equal([ + JSON.stringify( + { + extends: '../../tsconfig.build.json', + compilerOptions: { + outDir: 'dist', + rootDir: 'src', + tsBuildInfoFile: 'dist/tsconfig.tsbuildinfo', + }, + references: [ + { path: '../../depdendency-1' }, + { path: '../../depdendency-2' }, + ], + }, + null, + 2 + ), + { + ext: '.json', + }, + ]); + }); + + withStubs.test('Support custom config names', async ctx => { + ctx.stubbedReadFile + .withArgs('/path/to/ts/config/tsconfig.other.json', 'utf8') + .resolves(JSON.stringify({})); + ctx.stubbedReadFile + .withArgs('/path/to/depdendency-1/tsconfig.other.json', 'utf8') + .resolves( + JSON.stringify({ + references: [], + }) + ); + + ctx.stubbedTextFormatter.resolves(''); + + ctx.stubbedPopulateFile.resolves({ updated: false, filePath: '' }); + + expect( + await ctx.updateTsReferencesFactory.updateTsReferences({ + tsConfigPath: '/path/to/ts/config/tsconfig.other.json', + dependencyRootPaths: ['/path/to/depdendency-1/tsconfig.other.json'], + dryRun: false, + }) + ).to.equal(false); + + expect(ctx.stubbedTextFormatter.getCall(0).args).to.deep.equal([ + JSON.stringify( + { + references: [{ path: '../../depdendency-1/tsconfig.other.json' }], + }, + null, + 2 + ), + { + ext: '.json', + }, + ]); + }); + + withStubs.test('Ignore non-existent configs', async ctx => { + ctx.stubbedReadFile + .withArgs('/path/to/ts/config/tsconfig.json', 'utf8') + .resolves(JSON.stringify({})); + ctx.stubbedReadFile.withArgs('/path/to/depdendency-1/tsconfig.json', 'utf8').resolves( + JSON.stringify({ + references: [], + }) + ); + ctx.stubbedReadFile + .withArgs('/path/to/depdendency-2/tsconfig.json', 'utf8') + .rejects(new Error('')); + ctx.stubbedReadFile + .withArgs('/path/to/depdendency-3/tsconfig.json', 'utf8') + .resolves(JSON.stringify({ references: '' })); + + ctx.stubbedTextFormatter.resolves(''); + + ctx.stubbedPopulateFile.resolves({ + updated: true, + filePath: '', + reason: 'content-changed', + }); + + expect( + await ctx.updateTsReferencesFactory.updateTsReferences({ + tsConfigPath: '/path/to/ts/config/tsconfig.json', + dependencyRootPaths: [ + '/path/to/depdendency-1/tsconfig.json', + '/path/to/depdendency-2/tsconfig.json', + '/path/to/depdendency-3/tsconfig.json', + ], + dryRun: false, + }) + ).to.equal(true); + + expect(ctx.stubbedReadFile.callCount).to.equal(4); + + expect(ctx.stubbedTextFormatter.getCall(0).args).to.deep.equal([ + JSON.stringify( + { + references: [{ path: '../../depdendency-1' }], + }, + null, + 2 + ), + { + ext: '.json', + }, + ]); + }); + }); +});