update: upgrade dependencies and fix tests

- remove spectron
- use electron-mocha to run mocha test suites inside electron
This commit is contained in:
Xymorot 2020-12-28 19:58:20 +01:00
parent 4c169178d9
commit d5697540a8
23 changed files with 1227 additions and 1980 deletions

View File

@ -9,7 +9,6 @@
node_modules node_modules
.nyc_output .nyc_output
/src/**/*.js /src/**/*.js
/mocks/**/*.js
/frontend /frontend
/test-paths /test-paths
/out /out

View File

@ -43,7 +43,6 @@
"devDependencies": [ "devDependencies": [
"**/*.{spec,mock}.*", "**/*.{spec,mock}.*",
"src/**/test/*", "src/**/test/*",
"mocks/**/*",
"src/renderer/**/*", "src/renderer/**/*",
"templates/**/*", "templates/**/*",
"scripts/**/*" "scripts/**/*"
@ -154,8 +153,6 @@
"rules": { "rules": {
"no-unused-expressions": "off", "no-unused-expressions": "off",
"import/order": "off",
"@typescript-eslint/no-magic-numbers": "off", "@typescript-eslint/no-magic-numbers": "off",
"@typescript-eslint/typedef": "off", "@typescript-eslint/typedef": "off",
"@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-var-requires": "off",

2
.gitignore vendored
View File

@ -7,8 +7,6 @@ node_modules
# generated code # generated code
/src/**/*.js /src/**/*.js
/src/**/*.js.map /src/**/*.js.map
/mocks/**/*.js
/mocks/**/*.js.map
/frontend /frontend
# created by testing # created by testing

3
.mocharc.json Normal file
View File

@ -0,0 +1,3 @@
{
"spec": "src/**/*.spec.js"
}

View File

@ -1,2 +0,0 @@
# https://github.com/mochajs/mocha/blob/master/example/config
spec: 'src/**/*.spec.js'

View File

@ -16,7 +16,6 @@
node_modules node_modules
.nyc_output .nyc_output
/src/**/*.js /src/**/*.js
/mocks/**/*.js
/frontend /frontend
/test-paths /test-paths
/out /out

View File

@ -137,34 +137,27 @@ The point of these libraries is to make uniform code possible over various edito
The testing framework of choice is [Mocha](https://mochajs.org/). Call `npm run test` to run all tests. Tests are written in typescript and need to be transpiled before testing. The testing framework of choice is [Mocha](https://mochajs.org/). Call `npm run test` to run all tests. Tests are written in typescript and need to be transpiled before testing.
- assertion is (mainly) done by [Chai](https://www.chaijs.com/) - assertion is (mainly) done by [Chai](https://www.chaijs.com/)should not be compromised
- Electron specific testing is done by [Spectron](https://electronjs.org/spectron)
- it writes its electron files into a temporary directory, so the local app installation should not be compromised
- spies, stubs and mocks are provided by [Sinon.JS](https://sinonjs.org/) - spies, stubs and mocks are provided by [Sinon.JS](https://sinonjs.org/)
- HTTP server mocking is done by [nock](https://github.com/nock/nock) - HTTP server mocking is done by [nock](https://github.com/nock/nock)
- property based testing is made possible by [fast-check](https://github.com/dubzzz/fast-check) - property based testing is made possible by [fast-check](https://github.com/dubzzz/fast-check)
For the creation of test files look at existing ones, they are named `*.spec.ts`. For the creation of test files look at existing ones, they are named `*.spec.ts`. They are run inside electron via [electron-mocha](https://github.com/jprichardson/electron-mocha).
#### Mocks #### Mocks
There are 2 ways in which mocks are defined/used: Mocks are defined/used for own modules, just beside their file.
0. for external modules, in [mocks](mocks) - name the file `*.mock.ts` and use existing mock files for orientation on how to build them
- uses the [rewiremock](https://github.com/theKashey/rewiremock) package - use sparingly and only when not having a mock makes it more complex e.g. for modules which interact with the file system
- use this only when there is some magic happening like for electron which normally runs in its own node process
1. for own modules, just beside their file
- name the file `*.mock.ts` and use existing mock files for orientation on how to build them
- use sparingly and only when not having a mock makes it more complex e.g. for modules which interact with the file system
#### Tagging #### Tagging
Mocha does [not have a separate tagging feature](https://github.com/mochajs/mocha/wiki/Tagging), but it can filter via title. Use the following tags in your test titles: Mocha does [not have a separate tagging feature](https://github.com/mochajs/mocha/wiki/Tagging), but it can filter via title. Use the following tags in your test titles:
| tag | usage when | | tag | usage when |
| ----------- | ------------------------- | | ------- | ------------------------- |
| `@slow` | test is particularly slow | | `@slow` | test is particularly slow |
| `@spectron` | test uses spectron |
#### Coverage #### Coverage

View File

@ -1,28 +0,0 @@
import path from 'path';
import { rewiremock } from './rewiremock';
import WebContents = Electron.WebContents;
const electronMock: DeepPartial<typeof Electron> = {
app: {
on(): void {},
getPath(name: string): string {
return path.resolve('test-paths', name);
},
quit(): void {},
getAppPath(): string {
return path.resolve(__dirname, '..');
},
},
BrowserWindow: class {
public webContents: DeepPartial<WebContents> = {
openDevTools(): void {},
};
public loadFile(): void {}
public on(): void {}
},
ipcMain: {
on(): void {},
},
};
rewiremock('electron').with(electronMock);

View File

@ -1,6 +0,0 @@
import rewiremock from 'rewiremock';
rewiremock.overrideEntryPoint(module);
rewiremock.enable();
export { rewiremock };

2919
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,9 @@
}, },
"scripts": { "scripts": {
"postinstall": "npm run rebuild", "postinstall": "npm run rebuild",
"postupdate": "npm run rebuild",
"start": "electron . --enable-logging --env=dev", "start": "electron . --enable-logging --env=dev",
"rebuild": "electron-rebuild -f -b -t prod,dev,optional", "rebuild": "electron-rebuild -f -b -o better-sqlite3",
"electron-version": "electron scripts/electron-version.js", "electron-version": "electron scripts/electron-version.js",
"typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store", "typeorm:migrate": "npm run typeorm:migrate:library && npm run typeorm:migrate:store",
"typeorm:migrate:library": "typeorm migration:run -c library", "typeorm:migrate:library": "typeorm migration:run -c library",
@ -29,11 +30,10 @@
"dev:ts": "tsc -w --pretty --preserveWatchOutput", "dev:ts": "tsc -w --pretty --preserveWatchOutput",
"build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"", "build": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run build:webpack\" \"npm run build:index\" \"npm run build:ts\"",
"dev": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run dev:webpack\" \"npm run dev:index\" \"npm run dev:ts\"", "dev": "concurrently -c green,yellow,cyan -n webpack,index,typescript \"npm run dev:webpack\" \"npm run dev:index\" \"npm run dev:ts\"",
"test:fast": "mocha --grep \"@(slow|spectron)\" --invert", "test:fast": "electron-mocha --config .mocharc.json --grep \\\"@slow\\\" --invert",
"test": "mocha", "test": "electron-mocha --config .mocharc.json",
"coverage:fast": "nyc npm run test:fast",
"coverage": "nyc npm run test", "coverage": "nyc npm run test",
"prelint": "eslint --print-config scripts/forge.config.js | eslint-config-prettier-check", "prelint": "eslint-config-prettier src/main.ts",
"lint": "eslint ./**/*.* --max-warnings 1", "lint": "eslint ./**/*.* --max-warnings 1",
"lint:fix": "eslint ./**/*.* --fix", "lint:fix": "eslint ./**/*.* --fix",
"prettier": "prettier -c **/*.*", "prettier": "prettier -c **/*.*",
@ -41,11 +41,11 @@
"fix": "npm run lint:fix && npm run prettier:fix", "fix": "npm run lint:fix && npm run prettier:fix",
"forge:make": "electron-forge --platform win32 --arch x64 make", "forge:make": "electron-forge --platform win32 --arch x64 make",
"forge": "npm audit --production --audit-level=high && npm run build && npm run forge:make", "forge": "npm audit --production --audit-level=high && npm run build && npm run forge:make",
"precommit": "npm run prettier && npm run lint && npm run build && npm run coverage:fast", "precommit": "npm run prettier && npm run lint && npm run build && npm run test:fast",
"prepush": "npm run build && npm run coverage" "prepush": "npm run build && npm run coverage"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^7.1.1", "better-sqlite3": "^7.1.2",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"inversify": "^5.0.1", "inversify": "^5.0.1",
@ -53,7 +53,7 @@
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"typeorm": "^0.2.29", "typeorm": "^0.2.29",
"uuid": "^8.3.1" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.54", "@electron-forge/cli": "^6.0.0-beta.54",
@ -61,40 +61,41 @@
"@types/better-sqlite3": "^5.4.1", "@types/better-sqlite3": "^5.4.1",
"@types/chai": "^4.2.14", "@types/chai": "^4.2.14",
"@types/chai-fs": "^2.0.2", "@types/chai-fs": "^2.0.2",
"@types/fs-extra": "^9.0.3", "@types/fs-extra": "^9.0.6",
"@types/glob": "^7.1.3",
"@types/minimist": "^1.2.1", "@types/minimist": "^1.2.1",
"@types/mocha": "^8.0.3", "@types/mocha": "^8.2.0",
"@types/node": "^12.19.3", "@types/node": "^14.14.16",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/sinon": "^9.0.8", "@types/sinon": "^9.0.10",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"@types/webpack": "^4.41.24", "@types/webpack": "^4.41.25",
"@typescript-eslint/eslint-plugin": "^4.7.0", "@typescript-eslint/eslint-plugin": "^4.11.1",
"@typescript-eslint/parser": "^4.7.0", "@typescript-eslint/parser": "^4.11.1",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-fs": "^2.0.0", "chai-fs": "^2.0.0",
"chokidar": "^3.4.3", "chokidar": "^3.4.3",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"electron": "^10.1.5", "electron": "^10.2.0",
"electron-rebuild": "^2.3.2", "electron-mocha": "^10.0.0",
"eslint": "^7.13.0", "electron-rebuild": "^2.3.4",
"eslint-config-prettier": "^6.15.0", "eslint": "^7.16.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"fast-check": "^2.6.1", "fast-check": "^2.10.0",
"glob": "^7.1.6",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"husky": "^4.3.0", "husky": "^4.3.6",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"mocha": "^8.2.1", "mocha": "^8.2.1",
"nock": "^13.0.4", "nock": "^13.0.5",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.1.2", "prettier": "^2.2.1",
"rewiremock": "^3.14.3", "sinon": "^9.2.2",
"sinon": "^9.2.1", "svelte": "^3.31.0",
"spectron": "^12.0.0",
"svelte": "^3.29.6",
"svelte-loader": "^2.13.6", "svelte-loader": "^2.13.6",
"ts-loader": "^8.0.11", "ts-loader": "^8.0.12",
"typescript": "^4.0.5", "typescript": "^4.1.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"
}, },

View File

@ -6,7 +6,6 @@ const ignoreList = [
/^\/\.nyc_output($|\/)/, /^\/\.nyc_output($|\/)/,
/^\/declarations($|\/)/, /^\/declarations($|\/)/,
/^\/mocks($|\/)/,
/^\/templates($|\/)/, /^\/templates($|\/)/,
/^\/test-paths($|\/)/, /^\/test-paths($|\/)/,
/^\/types($|\/)/, /^\/types($|\/)/,
@ -15,15 +14,17 @@ const ignoreList = [
/^\/\.editorconfig/, /^\/\.editorconfig/,
/^\/\.eslintignore/, /^\/\.eslintignore/,
/^\/\.prettierignore/,
/^\/\.eslintrc\.json/, /^\/\.eslintrc\.json/,
/^\/\.gitignore/, /^\/\.gitignore/,
/^\/\.mocharc\.yml/, /^\/\.mocharc\.json/,
/^\/\.nycrc\.yml/, /^\/\.nycrc\.yml/,
/^\/\.prettierrc\.yml/, /^\/\.prettierrc\.yml/,
/^\/CONTRIBUTING\.md/, /^\/CONTRIBUTING\.md/,
/^\/ormconfig\.yml/, /^\/ormconfig\.yml/,
/^\/package-lock\.json/, /^\/package-lock\.json/,
/^\/tsconfig\.json/, /^\/tsconfig\.json/,
/^\/tsconfig\.renderer\.json/,
/^\/node_modules\/\.cache($|\/)/, /^\/node_modules\/\.cache($|\/)/,
// test and mock files: // test and mock files:
@ -36,8 +37,10 @@ const ignoreList = [
const name = packageJson.productName; const name = packageJson.productName;
if(name !== 'Renai') { if (name !== 'Renai') {
throw new TypeError(`The product name "${name}" in package.json is not "Renai"! Change it before building but do not commit it.`) throw new TypeError(
`The product name "${name}" in package.json is not "Renai"! Change it before building but do not commit it.`
);
} }
module.exports = { module.exports = {

View File

@ -1,32 +0,0 @@
import { expect } from 'chai';
import rewiremock from 'rewiremock';
import 'mocha';
import { IApplicationContext } from './main/test/i-application-context';
import { createApplication } from './main/test/spectron-util';
rewiremock.disable();
describe('Application @spectron', function () {
this.timeout(20000);
before(function (this: IApplicationContext) {
this.app = createApplication();
return this.app.start();
});
after(function (this: IApplicationContext) {
if (this.app && this.app.isRunning()) {
return this.app.stop();
}
});
it('shows an initial window', function (this: IApplicationContext) {
if (!this.app) {
throw Error('this.app is falsy');
}
return this.app.client.getWindowCount().then((count: number) => {
expect(count).to.be.gte(1);
});
});
});

View File

@ -1,19 +1,8 @@
import rewiremock from 'rewiremock';
import '../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import { Database, getConnection } from './database'; import { Database, getConnection } from './database';
describe('Database Service', () => { describe('Database Service', () => {
before(() => {
rewiremock.enable();
});
after(() => {
rewiremock.disable();
});
it('returns a connection', async () => { it('returns a connection', async () => {
const libraryConnection = await getConnection(Database.LIBRARY); const libraryConnection = await getConnection(Database.LIBRARY);
expect(libraryConnection).to.not.equal(undefined); expect(libraryConnection).to.not.equal(undefined);

View File

@ -1,27 +1,16 @@
import rewiremock from 'rewiremock';
import '../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import { isDev } from './env'; import { isDev } from './env';
export function setDev(dev = true): void { export function setDev(dev = true): void {
if (dev) { if (dev) {
process.argv.push('--env=dev'); process.argv.splice(2, 0, '--env=dev');
} else { } else {
process.argv = process.argv.filter((value) => value !== '--env=dev'); process.argv = process.argv.filter((value) => value !== '--env=dev');
} }
} }
describe('Environment Service', () => { describe('Environment Service', () => {
before(() => {
rewiremock.enable();
});
after(() => {
rewiremock.disable();
});
it('correctly identifies the development process argument', () => { it('correctly identifies the development process argument', () => {
setDev(); setDev();
expect(isDev()).to.be.true; expect(isDev()).to.be.true;

View File

@ -35,7 +35,7 @@ export abstract class UrlAppWindow extends AppWindow implements IUrlAppWindow {
const waitInterval = 1000; const waitInterval = 1000;
let failedLoad = true; let failedLoad = true;
do { do {
await new Promise((resolve) => { await new Promise<void>((resolve) => {
if (!this._window) { if (!this._window) {
throw new WindowClosedError(); throw new WindowClosedError();
} }

View File

@ -1,16 +1,13 @@
import rewiremock from 'rewiremock'; import { createInterface, Interface } from 'readline';
import '../../../../mocks/electron';
import chai, { expect } from 'chai'; import chai, { expect } from 'chai';
import 'mocha'; import 'mocha';
import chaiFs from 'chai-fs';
import fc from 'fast-check';
import fs from 'fs-extra';
import { container } from '../../core/container'; import { container } from '../../core/container';
import { setDev } from '../../core/env.spec'; import { setDev } from '../../core/env.spec';
import { ILogger } from './i-logger'; import { ILogger } from './i-logger';
import fs from 'fs-extra';
import { createInterface, Interface } from 'readline';
import fc from 'fast-check';
import { LogLevel } from './log-level'; import { LogLevel } from './log-level';
import chaiFs from 'chai-fs';
chai.use(chaiFs); chai.use(chaiFs);
@ -44,14 +41,6 @@ describe('Logger Service', () => {
const logLevelNumberArbitrary = fc.constantFrom(...logLevelsNumber); const logLevelNumberArbitrary = fc.constantFrom(...logLevelsNumber);
before(() => {
rewiremock.enable();
});
after(() => {
rewiremock.disable();
});
it('creates log files', () => { it('creates log files', () => {
const logger: ILogger = container.get('logger'); const logger: ILogger = container.get('logger');
@ -68,8 +57,18 @@ describe('Logger Service', () => {
it("default log file doesn't get bigger than 50KB @slow", async () => { it("default log file doesn't get bigger than 50KB @slow", async () => {
const logger: ILogger = container.get('logger'); const logger: ILogger = container.get('logger');
for (let i = 0; i < maxLogSize; i++) { let prevLogFileSize = (await fs.stat(logger.getLogFile())).size;
let minNumberOfLines = maxLogSize;
const sizeIncreases = [];
for (let i = 0; i <= minNumberOfLines; i++) {
await logger.log(4, 'your waifu is trash'); await logger.log(4, 'your waifu is trash');
const currLogFileSize = (await fs.stat(logger.getLogFile())).size;
const sizeIncrease = currLogFileSize - prevLogFileSize;
if (sizeIncrease) {
sizeIncreases.push(sizeIncrease);
minNumberOfLines = maxLogSize / (sizeIncreases.reduce((sum, e) => sum + e, 0) / sizeIncreases.length);
}
prevLogFileSize = currLogFileSize;
} }
const logFileStats = await fs.stat(logger.getLogFile()); const logFileStats = await fs.stat(logger.getLogFile());
@ -79,12 +78,22 @@ describe('Logger Service', () => {
it("exception log file doesn't get bigger than 50KB @slow", async () => { it("exception log file doesn't get bigger than 50KB @slow", async () => {
const logger: ILogger = container.get('logger'); const logger: ILogger = container.get('logger');
for (let i = 0; i < maxLogSize; ++i) { let prevLogFileSize = (await fs.stat(logger.getExceptionsLogFile())).size;
let minNumberOfLines = maxLogSize;
const sizeIncreases = [];
for (let i = 0; i <= minNumberOfLines; i++) {
await logger.exception(new Error('your waifu is trash')); await logger.exception(new Error('your waifu is trash'));
const currLogFileSize = (await fs.stat(logger.getExceptionsLogFile())).size;
const sizeIncrease = currLogFileSize - prevLogFileSize;
if (sizeIncrease) {
sizeIncreases.push(sizeIncrease);
minNumberOfLines = maxLogSize / (sizeIncreases.reduce((sum, e) => sum + e, 0) / sizeIncreases.length);
}
prevLogFileSize = currLogFileSize;
} }
const logFileStats = await fs.stat(logger.getLogFile()); const logFileStats = await fs.stat(logger.getExceptionsLogFile());
expect(logFileStats.size).lessThan(maxLogSize, 'log is bigger than its max size'); expect(logFileStats.size).lessThan(maxLogSize, 'exception log is bigger than its max size');
}).timeout(15000); }).timeout(15000);
it('logs different levels directly', () => { it('logs different levels directly', () => {

View File

@ -180,11 +180,17 @@ export class NhentaiAppWindow extends UrlAppWindow implements INhentaiAppWindow
} }
private async getBookTorrent(bookUrl: string): Promise<IFavorite> { private async getBookTorrent(bookUrl: string): Promise<IFavorite> {
if (!this._window) {
throw new WindowClosedError();
}
const galleryId = getGalleryId(bookUrl); const galleryId = getGalleryId(bookUrl);
const fileName = `${galleryId}.torrent`; const fileName = `${galleryId}.torrent`;
const filePath = path.resolve(os.tmpdir(), fileName); const filePath = path.resolve(os.tmpdir(), fileName);
await this.loadUrlSafe(bookUrl); await this.loadUrlSafe(bookUrl);
await new Promise<string>((resolve, reject) => { const downloadLink: string = (await this._window.webContents.executeJavaScript(
`document.getElementById('${downloadLinkId}').href`
)) as string;
await new Promise<void>((resolve, reject) => {
if (!this._window) { if (!this._window) {
throw new WindowClosedError(); throw new WindowClosedError();
} }
@ -206,7 +212,7 @@ export class NhentaiAppWindow extends UrlAppWindow implements INhentaiAppWindow
item.resume(); item.resume();
}); });
}); });
void this._window.webContents.executeJavaScript(`document.getElementById('${downloadLinkId}').click()`); void this.loadUrlSafe(downloadLink);
}); });
const readable = createReadStream(filePath, { emitClose: true }); const readable = createReadStream(filePath, { emitClose: true });

View File

@ -1,6 +1,3 @@
import rewiremock from 'rewiremock';
import '../../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import { container } from '../../core/container'; import { container } from '../../core/container';
@ -9,14 +6,6 @@ import { IStore } from './i-store';
describe('Store Service', function () { describe('Store Service', function () {
this.timeout(10000); this.timeout(10000);
before(() => {
rewiremock.enable();
});
after(() => {
rewiremock.disable();
});
it('loads saved data', () => { it('loads saved data', () => {
const store: IStore = container.get('store'); const store: IStore = container.get('store');
const testData = { const testData = {

View File

@ -1,6 +0,0 @@
import { Context } from 'mocha';
import { Application } from 'spectron';
export interface IApplicationContext extends Context {
app?: Application;
}

View File

@ -1,9 +0,0 @@
import * as electron from 'electron';
import { Application } from 'spectron';
export function createApplication(): Application {
return new Application({
path: ((electron as unknown) as { default: string }).default,
args: ['.', '--enable-logging'],
});
}

View File

@ -15,5 +15,5 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true "emitDecoratorMetadata": true
}, },
"include": ["declarations/**/*.ts", "types/**/*.ts", "src/**/*.ts", "mocks/**/*.ts"] "include": ["declarations/**/*.ts", "types/**/*.ts", "src/**/*.ts"]
} }

View File

@ -1,4 +1,8 @@
/** /**
* @see MethodDecorator * @see MethodDecorator
*/ */
type IpcControllerMethodDecorator = <T>(target: IIpcController, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => void type IpcControllerMethodDecorator = <T>(
target: IIpcController,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>
) => void;