Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d44e947

Browse files
authoredDec 11, 2023
Meta: Move feature checker to Vitest (refined-github#7145)
1 parent f5884e8 commit d44e947

File tree

13 files changed

+1156
-1062
lines changed

13 files changed

+1156
-1062
lines changed
 

‎.github/workflows/features.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
node-version-file: package.json
2020
cache: npm
2121
- run: npm ci
22-
- run: npm run test:features
22+
- run: npm run vitest -- --testNamePattern features.test
2323

2424
TestURLs:
2525
runs-on: ubuntu-latest
@@ -34,7 +34,7 @@ jobs:
3434
id: changed-files
3535
uses: tj-actions/changed-files@v35
3636
with:
37-
files: source/features/*.tsx
37+
files: source/features/*.{tsx,css}
3838

3939
- name: Every new/edited file must have a test URL
4040
run: bash build/verify-test-urls.sh ${{steps.changed-files.outputs.all_changed_files}}

‎.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
node-version-file: package.json
2626
cache: npm
2727
- run: npm ci
28-
- run: npm run vitest
28+
- run: npm run vitest -- --testNamePattern !features.test
2929

3030
Lint:
3131
runs-on: ubuntu-latest

‎.xo-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"no-alert": "off",
1515
"no-warning-comments": "off",
1616
"unicorn/no-nested-ternary": "off",
17+
"unicorn/better-regex": "off",
1718
"unicorn/prefer-top-level-await": "off",
1819
"unicorn/prefer-dom-node-dataset": "off",
1920
"unicorn/expiring-todo-comments": ["warn", {

‎build/__snapshots__/features-meta.json

Lines changed: 941 additions & 923 deletions
Large diffs are not rendered by default.

‎build/features.test.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import {test, describe, assert} from 'vitest';
2+
import {parse, join} from 'node:path';
3+
import {existsSync, readdirSync, readFileSync} from 'node:fs';
4+
import regexJoin from 'regex-join';
5+
import fastIgnore from 'fast-ignore';
6+
7+
import {isFeaturePrivate} from '../source/helpers/feature-utils.js';
8+
import {getImportedFeatures, getFeaturesMeta} from './readme-parser.js';
9+
10+
const isGitIgnored = fastIgnore(readFileSync('.gitignore', 'utf8'));
11+
12+
const noScreenshotExceptions = new Set([
13+
// Only add feature here if it's a shortcut only and/or extremely clear by name or description
14+
'sort-conversations-by-update-time',
15+
'prevent-pr-merge-panel-opening',
16+
'infinite-scroll',
17+
'command-palette-navigation-shortcuts',
18+
'comment-fields-keyboard-shortcuts',
19+
'copy-on-y',
20+
'create-release-shortcut',
21+
'pagination-hotkey',
22+
'profile-hotkey',
23+
'repo-wide-file-finder',
24+
'select-all-notifications-shortcut',
25+
'selection-in-new-tab',
26+
'submission-via-ctrl-enter-everywhere',
27+
28+
'hide-navigation-hover-highlight', // TODO: Add side-by-side gif
29+
'hide-inactive-deployments', // TODO: side-by-side png
30+
'esc-to-deselect-line', // TODO Add gif with key overlay
31+
'hide-newsfeed-noise', // TODO: Add side-by-side png
32+
'scrollable-areas', // TODO: Add side-by-side png
33+
]);
34+
35+
const entryPoint = 'source/refined-github.ts';
36+
const entryPointSource = readFileSync(entryPoint);
37+
const importedFeatures = getImportedFeatures();
38+
const featuresInReadme = getFeaturesMeta();
39+
40+
// We used to enforce the filetype, but this is no longer possible with new URLs
41+
// https://github.com/refined-github/refined-github/pull/7130
42+
const imageRegex = /\.(png|gif)$/;
43+
44+
const rghUploadsRegex = /refined-github[/]refined-github[/]assets[/]/;
45+
46+
const screenshotRegex = regexJoin(imageRegex, /|/, rghUploadsRegex);
47+
48+
class FeatureFile {
49+
readonly id: FeatureID;
50+
readonly path: string;
51+
constructor(readonly name: string) {
52+
this.id = parse(name).name as FeatureID;
53+
this.path = join('source/features', name);
54+
}
55+
56+
exists(): boolean {
57+
return existsSync(this.path);
58+
}
59+
60+
// eslint-disable-next-line n/prefer-global/buffer -- Type only
61+
contents(): Buffer {
62+
return readFileSync(this.path);
63+
}
64+
65+
get tsx(): FeatureFile {
66+
if (this.name.endsWith('.gql')) {
67+
const id = importedFeatures.find(featureId => this.id.startsWith(featureId));
68+
if (id) {
69+
return new FeatureFile(id + '.tsx');
70+
}
71+
}
72+
73+
return new FeatureFile(this.id + '.tsx');
74+
}
75+
76+
get css(): FeatureFile {
77+
return new FeatureFile(this.id + '.css');
78+
}
79+
}
80+
81+
function validateCss(file: FeatureFile): void {
82+
const isImportedByEntrypoint = entryPointSource.includes(`import './features/${file.name}';`);
83+
if (!file.tsx.exists()) {
84+
assert(
85+
isImportedByEntrypoint,
86+
`Should be imported by \`${entryPoint}\` or removed if it is not needed`,
87+
);
88+
return;
89+
}
90+
91+
assert(
92+
file.tsx.contents().includes(`import './${file.name}';`),
93+
`Should be imported by \`${file.tsx.name}\``,
94+
);
95+
96+
assert(
97+
!isImportedByEntrypoint,
98+
`Should only be imported by \`${file.tsx.name}\`, not by \`${entryPoint}\``,
99+
);
100+
}
101+
102+
function validateGql(file: FeatureFile): void {
103+
assert(
104+
file.tsx.exists(),
105+
'Does not match any existing features. The filename should match the feature that uses it.',
106+
);
107+
108+
assert(
109+
file.tsx.contents().includes(`from './${file.name}';`),
110+
`Should be imported by \`${file.tsx.name}\``,
111+
);
112+
}
113+
114+
function validateReadme(featureId: FeatureID): void {
115+
const [featureMeta, duplicate] = featuresInReadme.filter(feature => feature.id === featureId);
116+
assert(featureMeta, 'Should be described in the readme');
117+
118+
assert(
119+
featureMeta.description.length >= 20,
120+
'Should be described better in the readme (at least 20 characters)',
121+
);
122+
123+
assert(
124+
screenshotRegex.test(featureMeta.screenshot!)
125+
|| noScreenshotExceptions.has(featureId),
126+
'Should have a screenshot (png/gif) in the readme, unless really difficult to demonstrate (to be discussed in review)',
127+
);
128+
129+
assert(!duplicate, 'Should be described only once in the readme');
130+
}
131+
132+
function validateTsx(file: FeatureFile): void {
133+
assert(
134+
importedFeatures.includes(file.id),
135+
`Should be imported by \`${entryPoint}\``,
136+
);
137+
138+
const fileContents = readFileSync(`source/features/${file.name}`);
139+
140+
if (fileContents.includes('.addCssFeature')) {
141+
assert(
142+
!fileContents.includes('.add('),
143+
`${file.id} should use either \`addCssFeature\` or \`add\`, not both`,
144+
);
145+
146+
assert(
147+
file.css.exists(),
148+
`${file.id} uses \`.addCssFeature\`, but ${file.css.name} is missing`,
149+
);
150+
151+
assert(
152+
file.css.contents().includes(`[rgh-${file.id}]`),
153+
`${file.css.name} should contain a \`[rgh-${file.id}]\` selector`,
154+
);
155+
}
156+
157+
if (!isFeaturePrivate(file.name)) {
158+
validateReadme(file.id);
159+
}
160+
}
161+
162+
describe('features', async () => {
163+
const featuresDirContents = readdirSync('source/features/');
164+
test.each(featuresDirContents)('%s', filename => {
165+
if (isGitIgnored(filename)) {
166+
return;
167+
}
168+
169+
if (filename.endsWith('.gql')) {
170+
validateGql(new FeatureFile(filename));
171+
return;
172+
}
173+
174+
if (filename.endsWith('.css')) {
175+
validateCss(new FeatureFile(filename));
176+
return;
177+
}
178+
179+
if (filename.endsWith('.tsx')) {
180+
validateTsx(new FeatureFile(filename));
181+
return;
182+
}
183+
184+
assert.fail(`The \`/source/features\` folder should only contain .css, .tsx and .gql files. Found \`source/features/${filename}\``);
185+
});
186+
});

‎build/readme-parser.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
test('readme-parser', async () => {
99
await expect(getImportedFeatures().join('\n') + '\n')
1010
.toMatchFileSnapshot('./__snapshots__/imported-features.txt');
11-
await expect(JSON.parse(JSON.stringify(getFeaturesMeta())))
11+
const featuresMetaJson = JSON.stringify(
12+
getFeaturesMeta(),
13+
(_, value) => value ?? null, // Convert undefined to null to make them visible in the JSON
14+
'\t',
15+
);
16+
await expect(featuresMetaJson + '\n')
1217
.toMatchFileSnapshot('./__snapshots__/features-meta.json');
1318
});

‎build/readme-parser.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ import parseMarkdown from 'snarkdown';
77
// Group names must be unique because they will be merged
88
const simpleFeatureRegex = /^- \[]\(# "(?<simpleId>[^"]+)"\)(?: 🔥)? (?<simpleDescription>.+)$/gm;
99
const highlightedFeatureRegex = /<p><a title="(?<highlightedId>[^"]+)"><\/a> (?<highlightedDescripion>.+?)\n\t+<p><img src="(?<highlightedImage>.+?)">/g;
10-
// eslint-disable-next-line unicorn/better-regex -- ur wrong
1110
const featureRegex = regexJoin(simpleFeatureRegex, /|/, highlightedFeatureRegex);
1211
const imageRegex = /\.\w{3}$/; // 3 since .png and .gif have 3 letters
13-
// eslint-disable-next-line unicorn/better-regex -- ur dably rong
1412
const rghUploadsRegex = /refined-github[/]refined-github[/]assets[/]/;
15-
// eslint-disable-next-line unicorn/better-regex -- so tripoli wron
1613
const screenshotRegex = regexJoin(imageRegex, /|/, rghUploadsRegex);
1714

1815
function extractDataFromMatch(match: RegExpMatchArray): FeatureMeta {

‎build/verify-features.ts

Lines changed: 0 additions & 124 deletions
This file was deleted.

‎package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
"lint:prettier": "prettier . --check",
1616
"pack:safari": "xcodebuild -project 'safari/Refined GitHub.xcodeproj' -scheme 'Refined GitHub' -destination 'platform=macOS'",
1717
"prepare:safari": "bash build/prepare-safari-release.sh",
18-
"test": "run-p vitest lint:* build:* test:features --continue-on-error",
19-
"test:features": "tsx build/verify-features.ts",
18+
"test": "run-p vitest lint:* build:* --continue-on-error",
2019
"watch": "run-p watch:* --continue-on-error",
2120
"watch:typescript": "tsc --noEmit --watch --preserveWatchOutput",
2221
"watch:webpack": "npm run x:webpack -- --mode=development --watch",
@@ -63,6 +62,7 @@
6362
"dom-loaded": "^3.0.0",
6463
"doma": "^4.0.0",
6564
"element-ready": "^6.2.2",
65+
"fast-ignore": "^1.1.1",
6666
"filter-altered-clicks": "^2.0.1",
6767
"fit-textarea": "^2.0.0",
6868
"flat-zip": "^1.0.1",

‎source/features/rgh-feature-descriptions.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ function init(signal: AbortSignal): void {
139139
observe('#repos-sticky-header', add, {signal});
140140
}
141141

142-
// eslint-disable-next-line unicorn/better-regex -- No "\/"
143142
const featureUrlRegex = /^([/]refined-github){2}[/]blob[/][^/]+[/]source[/]features[/][^.]+[.](tsx|css)$/;
144143

145144
void features.add(import.meta.url, {

‎source/github-helpers/prevent-link-loss.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ function getRepoReference(currentRepo: RepositoryInfo | undefined, repoNameWithO
88
}
99

1010
const escapeRegex = (string: string): string => string.replaceAll(/[\\^$.*+?()[\]{}|]/g, '\\$&');
11-
const prCommitPathnameRegex = /[/]([^/]+[/][^/]+)[/]pull[/](\d+)[/]commits[/]([\da-f]{7})[\da-f]{33}(?:#[\w-]+)?\b/; // eslint-disable-line unicorn/better-regex
11+
const prCommitPathnameRegex = /[/]([^/]+[/][^/]+)[/]pull[/](\d+)[/]commits[/]([\da-f]{7})[\da-f]{33}(?:#[\w-]+)?\b/;
1212
export const prCommitUrlRegex = new RegExp('\\b' + escapeRegex(location.origin) + prCommitPathnameRegex.source, 'gi');
1313

14-
const prComparePathnameRegex = /[/]([^/]+[/][^/]+)[/]compare[/](.+)(#diff-[\da-fR-]+)/; // eslint-disable-line unicorn/better-regex
14+
const prComparePathnameRegex = /[/]([^/]+[/][^/]+)[/]compare[/](.+)(#diff-[\da-fR-]+)/;
1515
export const prCompareUrlRegex = new RegExp('\\b' + escapeRegex(location.origin) + prComparePathnameRegex.source, 'gi');
1616

17-
const discussionPathnameRegex = /[/]([^/]+[/][^/]+)[/]discussions[/](\d+)[?][^#\s]+(#[\w-]+)?\b/; // eslint-disable-line unicorn/better-regex
17+
const discussionPathnameRegex = /[/]([^/]+[/][^/]+)[/]discussions[/](\d+)[?][^#\s]+(#[\w-]+)?\b/;
1818
export const discussionUrlRegex = new RegExp('\\b' + escapeRegex(location.origin) + discussionPathnameRegex.source, 'gi');
1919

2020
// To be used as replacer callback in string.replace() for PR commit links

‎source/resolve-conflicts.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable @typescript-eslint/consistent-type-definitions -- Declaration merging necessary */
2-
/* eslint-disable unicorn/better-regex -- Go home you're drunk */
32

43
import regexJoin from 'regex-join';
54
import type CodeMirror from 'codemirror';
@@ -129,4 +128,3 @@ function acceptBranch(branch: string, line: number): void {
129128
editor.setCursor(linesToRemove[0]);
130129
}
131130

132-
/* eslint-enable unicorn/better-regex */

0 commit comments

Comments
 (0)
Please sign in to comment.