Skip to content

Commit 35d5f34

Browse files
committed
Add tests to the workflow
1 parent c2ca1b9 commit 35d5f34

File tree

12 files changed

+212
-23
lines changed

12 files changed

+212
-23
lines changed

.github/actions/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
"devDependencies": {
66
"@sourceacademy/modules-repotools": "workspace:^",
77
"@types/node": "^22.15.30",
8-
"typescript": "^5.8.2"
8+
"typescript": "^5.8.2",
9+
"vitest": "^3.2.3"
910
},
1011
"dependencies": {
1112
"@actions/core": "^1.11.1",
1213
"@actions/exec": "^1.1.1"
1314
},
1415
"scripts": {
1516
"build": "node ./build.js",
16-
"postinstall": "yarn build"
17+
"postinstall": "yarn build",
18+
"test": "vitest"
1719
}
1820
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import fs from 'fs/promises';
2+
import * as exec from '@actions/exec';
3+
import { describe, expect, it, test, vi } from 'vitest';
4+
import { checkForChanges, getPackageInfo } from '../index.js';
5+
6+
const mockedExecOutput = vi.spyOn(exec, 'getExecOutput');
7+
const mockedFsReadFile = vi.spyOn(fs, 'readFile');
8+
9+
describe('Test checkForChanges', () => {
10+
it('should return true if program exits with non zero code', async () => {
11+
mockedExecOutput.mockResolvedValueOnce({
12+
exitCode: 1, stdout: '', stderr: ''
13+
});
14+
15+
await expect(checkForChanges('/')).resolves.toEqual(true);
16+
expect(mockedExecOutput).toHaveBeenCalledOnce();
17+
});
18+
19+
it('should return false if program exits with 0', async () => {
20+
mockedExecOutput.mockResolvedValueOnce({
21+
exitCode: 0, stdout: '', stderr: ''
22+
});
23+
24+
await expect(checkForChanges('/')).resolves.toEqual(false);
25+
expect(mockedExecOutput).toHaveBeenCalledOnce();
26+
});
27+
});
28+
29+
describe('Test getPackageInfo', () => {
30+
/**
31+
* Use for momentarily mocking the loading of a package.json
32+
* file
33+
*/
34+
function mockPackageJson(name: string, needsPlaywright: boolean, changes: boolean) {
35+
const data: Record<string, unknown> = {
36+
name
37+
};
38+
39+
if (needsPlaywright) {
40+
data.devDependencies = {
41+
playwright: '^1.54.0'
42+
};
43+
}
44+
45+
mockedFsReadFile.mockResolvedValueOnce(JSON.stringify(data));
46+
mockedExecOutput.mockResolvedValueOnce({
47+
exitCode: changes ? 1 : 0, stdout: '', stderr: ''
48+
});
49+
}
50+
51+
test('regular loading of bundle', async () => {
52+
mockPackageJson('@sourceacademy/bundle-bundle0', false, true);
53+
54+
return expect(getPackageInfo('/')).resolves.toMatchObject({
55+
directory: '/',
56+
changes: true,
57+
needsPlaywright: false,
58+
bundleName: 'bundle0'
59+
});
60+
});
61+
62+
test('regular loading of tab without playwright', () => {
63+
mockPackageJson('@sourceacademy/tab-Tab0', false, true);
64+
return expect(getPackageInfo('/')).resolves.toMatchObject({
65+
directory: '/',
66+
changes: true,
67+
needsPlaywright: false,
68+
tabName: 'Tab0'
69+
});
70+
});
71+
72+
test('regular loading of a library without playwright', () => {
73+
mockPackageJson('@sourceacademy/modules-lib', false, true);
74+
return expect(getPackageInfo('/')).resolves.toMatchObject({
75+
directory: '/',
76+
changes: true,
77+
needsPlaywright: false,
78+
});
79+
});
80+
81+
test('loading a library without changes', () => {
82+
mockPackageJson('@sourceacademy/modules-lib', false, false);
83+
return expect(getPackageInfo('/')).resolves.toMatchObject({
84+
directory: '/',
85+
changes: false,
86+
needsPlaywright: false,
87+
});
88+
});
89+
90+
test('loading a library that needs playwright', () => {
91+
mockedExecOutput.mockResolvedValueOnce({
92+
exitCode: 1, stdout: '', stderr: ''
93+
});
94+
95+
mockPackageJson('@sourceacademy/modules-lib', true, true);
96+
return expect(getPackageInfo('/')).resolves.toMatchObject({
97+
directory: '/',
98+
changes: true,
99+
needsPlaywright: true,
100+
});
101+
});
102+
103+
test('loading an unknown package', () => {
104+
mockPackageJson('unknown-package', false, false);
105+
return expect(getPackageInfo('/')).rejects.toThrowError('Failed to match package name: unknown-package');
106+
});
107+
});

.github/actions/src/info/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ outputs:
1010
description: The list of tabs packages present
1111
devserver:
1212
description: Information for the devserver
13+
workflows:
14+
description: Did the worflow file change?
1315

1416
runs:
1517
using: node20
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { getExecOutput } from '@actions/exec';
2+
3+
export default async function getGitRoot() {
4+
const { stdout } = await getExecOutput('git rev-parse --show-toplevel');
5+
return stdout.trim();
6+
}

.github/actions/src/info/index.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import pathlib from 'path';
33
import utils from 'util';
44
import * as core from '@actions/core';
55
import { getExecOutput } from '@actions/exec';
6+
import getGitRoot from './gitRoot.js';
67

78
interface BasePackageRecord {
89
directory: string
@@ -26,7 +27,7 @@ type PackageRecord = BundlePackageRecord | TabPackageRecord | BasePackageRecord;
2627
* the master branch\
2728
* Used to determine, particularly for libraries if running tests and tsc are necessary
2829
*/
29-
async function checkForChanges(directory: string) {
30+
export async function checkForChanges(directory: string) {
3031
const { exitCode } = await getExecOutput(
3132
'git',
3233
['--no-pager', 'diff', '--quiet', 'origin/master', '--', directory],
@@ -44,8 +45,9 @@ const packageNameRE = /^@sourceacademy\/(.+?)-(.+)$/u;
4445
* Retrieves the information for a given package.
4546
* @param directory The directory containing the `package.json` for the given package.
4647
*/
47-
async function getPackageInfo(directory: string): Promise<PackageRecord> {
48-
const { default: { name, devDependencies } } = await import(`${directory}/package.json`, { with: { type: 'json' } });
48+
export async function getPackageInfo(directory: string): Promise<PackageRecord> {
49+
const fileName = pathlib.join(directory, 'package.json');
50+
const { name, devDependencies } = JSON.parse(await fs.readFile(fileName, 'utf8'));
4951
const match = packageNameRE.exec(name);
5052

5153
if (!match) {
@@ -54,31 +56,32 @@ async function getPackageInfo(directory: string): Promise<PackageRecord> {
5456

5557
const changes = await checkForChanges(directory);
5658
const [,packageType, packageBaseName] = match;
59+
const needsPlaywright = !!devDependencies && 'playwright' in devDependencies;
5760

5861
switch (packageType) {
5962
case 'bundle':
6063
return {
61-
directory: directory,
6264
changes,
65+
directory: directory,
6366
name,
64-
needsPlaywright: false,
67+
needsPlaywright,
6568
bundleName: packageBaseName,
6669
};
6770

6871
case 'tab':
6972
return {
70-
directory: directory,
7173
changes,
74+
directory: directory,
7275
name,
73-
needsPlaywright: 'playwright' in devDependencies,
76+
needsPlaywright,
7477
tabName: packageBaseName,
7578
};
7679
default: {
7780
return {
78-
directory: directory,
7981
changes,
82+
directory: directory,
8083
name,
81-
needsPlaywright: 'playwright' in devDependencies
84+
needsPlaywright
8285
};
8386
}
8487
}
@@ -105,7 +108,7 @@ async function findPackages(directory: string, maxDepth?: number) {
105108
throw error;
106109
}
107110

108-
if ('code' in error && error.code !== 'ERR_MODULE_NOT_FOUND') {
111+
if ('code' in error && error.code !== 'ENOENT') {
109112
core.error(error);
110113
throw error;
111114
}
@@ -164,17 +167,18 @@ async function runForAllPackages(gitRoot: string) {
164167
}
165168

166169
async function main() {
167-
const { stdout } = await getExecOutput('git rev-parse --show-toplevel');
168-
const gitRoot = stdout.trim();
170+
const gitRoot = await getGitRoot();
169171

170172
await runForAllPackages(gitRoot);
171-
// const fullPath = pathlib.join(gitRoot, query);
172-
// const packageInfo = await getPackageInfo(fullPath);
173-
// core.setOutput()
173+
174+
const workflows = await checkForChanges(pathlib.join(gitRoot, '.github/workflows'));
175+
core.setOutput('workflows', workflows);
174176
}
175177

176-
try {
177-
await main();
178-
} catch (error: any) {
179-
core.setFailed(error.message);
178+
if (process.env.GITHUB_ACTIONS) {
179+
try {
180+
await main();
181+
} catch (error: any) {
182+
core.setFailed(error.message);
183+
}
180184
}

.github/actions/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"include": ["src"],
2+
"include": ["src/**/*.ts", "vitest.setup.ts"],
33
"compilerOptions": {
44
"forceConsistentCasingInFileNames": true,
55
"lib": ["ES2020"],

.github/actions/vitest.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @ts-check
2+
// Actions Vitest config
3+
4+
import { baseVitestConfig } from '@sourceacademy/modules-repotools/testing';
5+
import { defineProject, mergeConfig } from 'vitest/config';
6+
7+
export default mergeConfig(
8+
baseVitestConfig,
9+
defineProject({
10+
test: {
11+
name: 'Github Actions',
12+
root: import.meta.dirname,
13+
include: ['./src/**/__tests__/*.test.ts'],
14+
setupFiles: ['vitest.setup.ts'],
15+
},
16+
})
17+
);

.github/actions/vitest.setup.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { summary } from '@actions/core';
2+
import { vi } from 'vitest';
3+
4+
vi.mock(import('@actions/core'), async importOriginal => {
5+
const original = await importOriginal();
6+
const outputObject: Record<string, any> = {};
7+
8+
const summaryObject: typeof summary = {
9+
addList() { return this; },
10+
addHeading() { return this; },
11+
write() {
12+
return Promise.resolve(this);
13+
}
14+
} as any;
15+
16+
return {
17+
...original,
18+
info: vi.fn(console.info),
19+
error: vi.fn(console.error),
20+
outputObject,
21+
setOutput: (p, v) => {
22+
outputObject[p] = JSON.stringify(v);
23+
},
24+
summary: summaryObject
25+
};
26+
});
27+
28+
vi.mock(import('@actions/exec'));
29+
30+
vi.mock(import('./src/info/gitRoot.js'), () => ({
31+
default: () => Promise.resolve('/')
32+
}));

.github/workflows/pull-request.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ jobs:
2727
cd ./.github/actions
2828
yarn build
2929
30+
- name: Test Actions
31+
run: |
32+
cd ./.github/actions
33+
yarn test
34+
3035
- name: Fetch Master branch
3136
run: git fetch origin master
3237

@@ -39,6 +44,7 @@ jobs:
3944
libs: ${{ steps.info.outputs.libs }}
4045
tabs: ${{ steps.info.outputs.tabs }}
4146
devserver: ${{ steps.info.outputs.devserver }}
47+
workflows: ${{ steps.info.outputs.workflows }}
4248

4349
libraries:
4450
name: Libraries
@@ -99,10 +105,13 @@ jobs:
99105
key: ${{ matrix.tabInfo.name }}
100106

101107
- name: Run Tests
108+
# https://github.com/vitest-dev/vitest/issues/5477
109+
# Known Vitest issue for coverage running in browser mode
110+
# Momentarily disable coverage checking on Github Actions for tabs
102111
if: matrix.tabInfo.changes
103112
run: |
104113
cd ${{ matrix.tabInfo.directory }}
105-
yarn test --coverage
114+
yarn test
106115
107116
- name: Run Auxillary Tasks
108117
if: matrix.tabInfo.changes

lib/lintplugin/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,25 @@
1313
"eslint": "^9.31.0"
1414
},
1515
"peerDependencies": {
16+
"@eslint/markdown": "^6.6.0",
1617
"@stylistic/eslint-plugin": "^4.4.1",
1718
"@vitest/eslint-plugin": "^1.3.4",
1819
"eslint": ">=9",
1920
"eslint-plugin-import": "^2.32.0",
21+
"eslint-plugin-jsdoc": "^51.3.1",
2022
"eslint-plugin-react": "^7.37.4",
2123
"eslint-plugin-react-hooks": "^5.1.0",
2224
"globals": "^15.11.0",
2325
"typescript-eslint": "^8.33.1"
2426
},
2527
"devDependencies": {
28+
"@eslint/markdown": "^6.6.0",
2629
"@sourceacademy/modules-buildtools": "workspace:^",
2730
"@sourceacademy/modules-repotools": "workspace:^",
2831
"@stylistic/eslint-plugin": "^4.4.1",
2932
"@vitest/eslint-plugin": "^1.3.4",
3033
"eslint-plugin-import": "^2.32.0",
34+
"eslint-plugin-jsdoc": "^51.3.1",
3135
"eslint-plugin-react": "^7.37.4",
3236
"eslint-plugin-react-hooks": "^5.1.0",
3337
"globals": "^15.11.0",

0 commit comments

Comments
 (0)