From e5376b34698352cb65bed79c4edc7167e3e4bc27 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Wed, 14 Sep 2022 13:55:04 +0200 Subject: [PATCH] Add DOM tests for variant analysis header --- extensions/ql-vscode/.eslintrc.js | 2 +- extensions/ql-vscode/jest.config.ts | 6 +- extensions/ql-vscode/package-lock.json | 254 ++++++++++++++++++ extensions/ql-vscode/package.json | 2 + .../VariantAnalysisHeader.tsx | 4 +- .../__tests__/VariantAnalysisHeader.spec.tsx | 80 +++++- extensions/ql-vscode/test/jest.setup.ts | 2 + 7 files changed, 335 insertions(+), 15 deletions(-) diff --git a/extensions/ql-vscode/.eslintrc.js b/extensions/ql-vscode/.eslintrc.js index 163205ec2..ce147b0dc 100644 --- a/extensions/ql-vscode/.eslintrc.js +++ b/extensions/ql-vscode/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { node: true, es6: true, }, - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jest-dom/recommended"], rules: { "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-unused-vars": [ diff --git a/extensions/ql-vscode/jest.config.ts b/extensions/ql-vscode/jest.config.ts index 7ac7b9e8c..444703982 100644 --- a/extensions/ql-vscode/jest.config.ts +++ b/extensions/ql-vscode/jest.config.ts @@ -14,7 +14,7 @@ export default { // cacheDirectory: "/private/var/folders/6m/1394pht172qgd7dmw1fwjk100000gn/T/jest_dx", // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true, + // clearMocks: true, // Indicates whether the coverage information should be collected while executing the test // collectCoverage: false, @@ -134,10 +134,10 @@ export default { // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test - setupFiles: ['/test/jest.setup.ts'], + // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ['/test/jest.setup.ts'], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index e7976a70b..49d396a90 100644 --- a/extensions/ql-vscode/package-lock.json +++ b/extensions/ql-vscode/package-lock.json @@ -58,6 +58,7 @@ "@storybook/manager-webpack5": "^6.5.10", "@storybook/react": "^6.5.10", "@storybook/testing-library": "^0.0.13", + "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", "@types/chai": "^4.1.7", "@types/chai-as-promised": "~7.1.2", @@ -105,6 +106,7 @@ "css-loader": "~3.1.0", "del": "^6.0.0", "eslint": "~6.8.0", + "eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-react": "~7.19.0", "eslint-plugin-storybook": "^0.6.4", "file-loader": "^6.2.0", @@ -143,6 +145,12 @@ "vscode": "^1.59.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -12364,6 +12372,108 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@testing-library/react": { "version": "12.1.5", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", @@ -13444,6 +13554,15 @@ "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", "dev": true }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, "node_modules/@types/through2": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/@types/through2/-/through2-2.0.36.tgz", @@ -18619,6 +18738,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/css/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -20480,6 +20605,25 @@ "node": "^8.10.0 || ^10.13.0 || >=11.10.1" } }, + "node_modules/eslint-plugin-jest-dom": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-4.0.2.tgz", + "integrity": "sha512-Jo51Atwyo2TdcUncjmU+UQeSTKh3sc2LF/M5i/R3nTU0Djw9V65KGJisdm/RtuKhy2KH/r7eQ1n6kwYFPNdHlA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.16.3", + "@testing-library/dom": "^8.11.1", + "requireindex": "^1.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/eslint-plugin-react": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", @@ -39577,6 +39721,12 @@ } }, "dependencies": { + "@adobe/css-tools": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -48639,6 +48789,84 @@ } } }, + "@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "@testing-library/react": { "version": "12.1.5", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", @@ -49653,6 +49881,15 @@ "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", "dev": true }, + "@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "requires": { + "@types/jest": "*" + } + }, "@types/through2": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/@types/through2/-/through2-2.0.36.tgz", @@ -53776,6 +54013,12 @@ "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", "dev": true }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -55326,6 +55569,17 @@ } } }, + "eslint-plugin-jest-dom": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-4.0.2.tgz", + "integrity": "sha512-Jo51Atwyo2TdcUncjmU+UQeSTKh3sc2LF/M5i/R3nTU0Djw9V65KGJisdm/RtuKhy2KH/r7eQ1n6kwYFPNdHlA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.16.3", + "@testing-library/dom": "^8.11.1", + "requireindex": "^1.2.0" + } + }, "eslint-plugin-react": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index e0a39f6df..d90fd43ef 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1250,6 +1250,7 @@ "@storybook/manager-webpack5": "^6.5.10", "@storybook/react": "^6.5.10", "@storybook/testing-library": "^0.0.13", + "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", "@types/chai": "^4.1.7", "@types/chai-as-promised": "~7.1.2", @@ -1297,6 +1298,7 @@ "css-loader": "~3.1.0", "del": "^6.0.0", "eslint": "~6.8.0", + "eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-react": "~7.19.0", "eslint-plugin-storybook": "^0.6.4", "file-loader": "^6.2.0", diff --git a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx index 66c3230c9..7dd509dcf 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/VariantAnalysisHeader.tsx @@ -6,7 +6,7 @@ import { LinkIconButton } from './LinkIconButton'; import styled from 'styled-components'; import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; -type Props = { +export type VariantAnalysisHeaderProps = { queryName: string; queryFileName: string; status: VariantAnalysisStatus; @@ -48,7 +48,7 @@ export const VariantAnalysisHeader = ({ onStopQueryClick, onCopyRepositoryListClick, onExportResultsClick -}: Props) => { +}: VariantAnalysisHeaderProps) => { return ( diff --git a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisHeader.spec.tsx b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisHeader.spec.tsx index 0806011eb..7848cac80 100644 --- a/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisHeader.spec.tsx +++ b/extensions/ql-vscode/src/view/variant-analysis/__tests__/VariantAnalysisHeader.spec.tsx @@ -1,21 +1,83 @@ import * as React from 'react'; -import { VariantAnalysisHeader } from '../VariantAnalysisHeader'; -import { render } from '@testing-library/react'; +import { VariantAnalysisHeader, VariantAnalysisHeaderProps } from '../VariantAnalysisHeader'; +import { render as reactRender, screen } from '@testing-library/react'; import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis'; +import { userEvent } from '@storybook/testing-library'; describe(VariantAnalysisHeader.name, () => { - it('renders correctly', () => { - render( + const onOpenQueryClick = jest.fn(); + const onViewQueryClick = jest.fn(); + const onStopQueryClick = jest.fn(); + const onCopyRepositoryListClick = jest.fn(); + const onExportResultsClick = jest.fn(); + + afterEach(() => { + onOpenQueryClick.mockReset(); + onViewQueryClick.mockReset(); + onStopQueryClick.mockReset(); + onCopyRepositoryListClick.mockReset(); + onExportResultsClick.mockReset(); + }); + + const render = (props: Partial = {}) => + reactRender( ); + + it('renders correctly', () => { + render(); + + expect(screen.getByText('Query name')).toBeInTheDocument(); + }); + + it('renders the query file name as a button', () => { + render(); + + userEvent.click(screen.getByText('example.ql')); + expect(onOpenQueryClick).toHaveBeenCalledTimes(1); + }); + + it('renders a view query button', () => { + render(); + + userEvent.click(screen.getByText('View query')); + expect(onViewQueryClick).toHaveBeenCalledTimes(1); + }); + + it('renders the stop query button when in progress', () => { + render({ status: VariantAnalysisStatus.InProgress }); + + userEvent.click(screen.getByText('Stop query')); + expect(onStopQueryClick).toHaveBeenCalledTimes(1); + }); + + it('renders the copy repository list button when succeeded', () => { + render({ status: VariantAnalysisStatus.Succeeded }); + + userEvent.click(screen.getByText('Copy repository list')); + expect(onCopyRepositoryListClick).toHaveBeenCalledTimes(1); + }); + + it('renders the export results button when succeeded', () => { + render({ status: VariantAnalysisStatus.Succeeded }); + + userEvent.click(screen.getByText('Export results')); + expect(onExportResultsClick).toHaveBeenCalledTimes(1); + }); + + it('does not render any buttons when failed', () => { + const { container } = render({ status: VariantAnalysisStatus.Failed }); + + expect(container.querySelectorAll('vscode-button').length).toEqual(0); }); }); diff --git a/extensions/ql-vscode/test/jest.setup.ts b/extensions/ql-vscode/test/jest.setup.ts index 09c8a31a1..2375aea05 100644 --- a/extensions/ql-vscode/test/jest.setup.ts +++ b/extensions/ql-vscode/test/jest.setup.ts @@ -1,3 +1,5 @@ +import '@testing-library/jest-dom'; + // https://jestjs.io/docs/26.x/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom Object.defineProperty(window, 'matchMedia', { writable: true,