Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
26b5e77
feat: Jest を Vitest に移行(Issue #1017)- WIP
hakatashi Jun 28, 2026
88b26b5
feat: Jest → Vitest 移行を完了(Issue #1017)
hakatashi Jun 29, 2026
8743370
fix: tsgo 型チェックエラーを解消(CI 修正)
hakatashi Jun 29, 2026
098ac75
fix: slackCache テストのPromise解決順序を決定論的にする(CI 修正)
hakatashi Jun 29, 2026
264567a
refactor: remove unused achievements mock and streamline mutex usage …
hakatashi Jun 29, 2026
fdbd0bc
Remove unused pino-std-serializers configuration from Vitest setup
hakatashi Jun 29, 2026
846aac0
fix: Vitest移行後のコードスタイルとSonarQube問題を修正
hakatashi Jun 29, 2026
0f3698e
chore: CJS起因のJSスタブファイルにESM化後の削除リマインダを追加
hakatashi Jun 29, 2026
43b56f0
chore: JSスタブファイルの削除リマインダにIssue番号 (#846) を追記
hakatashi Jun 29, 2026
cbe966e
fix: update comments to clarify test environment usage and remove unu…
hakatashi Jun 29, 2026
a79675c
Include test files to typescript project files and fix type issues
hakatashi Jun 29, 2026
f429295
Remove unused vitest.setup.ts
hakatashi Jun 29, 2026
6427adf
Remove reviewdog from test workflow and fix codecov to use official a…
hakatashi Jun 29, 2026
1cc4ba7
fix: update codecov action to v7 and enable verbose output
hakatashi Jun 29, 2026
83fc227
fix: update eslint extends to use @hakatashi/eslint-config/typescript
hakatashi Jun 30, 2026
e035dfe
refactor: clean up imports and improve type definitions in shogi module
hakatashi Jun 30, 2026
aecb8e2
sushi-bot: Type mocked moment module properly
hakatashi Jun 30, 2026
09766ae
Restore original implementations in some typescript files
hakatashi Jun 30, 2026
0cae9c1
fix: simplify property check in getReading mock
hakatashi Jun 30, 2026
f604335
Tidy up test compositions
hakatashi Jun 30, 2026
85b18b6
helloworld: Fix test
hakatashi Jun 30, 2026
5f5dfd6
feat: enhance fs mock with writeFile and mkdir implementations; updat…
hakatashi Jun 30, 2026
cbde26b
Fix SonarQube issues
hakatashi Jun 30, 2026
9973cec
vocabwar: Fix type issues
hakatashi Jun 30, 2026
afc4d8c
shogi: Fix Array class construction issue
hakatashi Jun 30, 2026
65735f1
chore: mock scrape-it in __mocks__ for lyrics and room-gacha tests
hakatashi Jul 7, 2026
bd913a0
Merge remote-tracking branch 'origin/master' into feat/vitest-migration
hakatashi Jul 8, 2026
bb0b7cb
fix: resolve vitest-migration conflicts and lint errors from merged m…
hakatashi Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,12 @@ jobs:
env:
NODE_OPTIONS: --trace-warnings --trace-deprecation --trace-exit --trace-uncaught --unhandled-rejections=strict --max-old-space-size=8192

- name: Set up reviewdog
if: ${{ github.event_name == 'pull_request' }}
uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest

- name: Run reviewdog
continue-on-error: true
if: ${{ github.event_name == 'pull_request' }}
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git ls-files | grep eslintrc | xargs -L 1 dirname | paste -sd ' ' | xargs -I {} sh -c "npx eslint --ext js,ts -f rdjson {} | reviewdog -f=rdjson -name=ESLint -reporter=github-pr-review"

- name: codecov
continue-on-error: true
run: npx codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true

test-rust:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ gha-creds-*.json

# Local Claude instructions
CLAUDE.local.md

# Runtime-generated state files
vocabwar/state.json
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ is not necessary because the code is self-explanatory. Instead, you must just re

### Testing

- Write tests using Jest framework
- Write tests using Vitest framework
- Follow existing test patterns and structure
- Use the provided `SlackMock` class for testing Slack interactions
- Place tests in `*.test.ts` files
- Run tests with `npm test -- [<test-file>]`
- Run tests with `npx vitest run [<test-file>]`

### Development Environment

Expand Down
4 changes: 1 addition & 3 deletions __mocks__/axios.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/* eslint-env node, jest */

const {PassThrough} = require('stream');

const axios = jest.fn((options = {}) => {
const axios = vi.fn((options = {}) => {
if (options.responseType === 'stream') {
const stream = new PassThrough();
process.nextTick(() => {
Expand Down
11 changes: 5 additions & 6 deletions __mocks__/cloudinary.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-env node, jest */

const {PassThrough} = require('stream');
const noop = require('lodash/noop');

const cloudinary = jest.genMockFromModule('cloudinary');
const cloudinary = {
v2: {uploader: {}, config: () => {}, url: vi.fn(() => '')},
url: '',
};

cloudinary.v2.uploader.upload_stream = jest.fn((options, callback) => {
cloudinary.v2.uploader.upload_stream = vi.fn((options, callback) => {
const stream = new PassThrough();
stream.on('end', () => {
callback(null, {
Expand All @@ -17,6 +18,4 @@ cloudinary.v2.uploader.upload_stream = jest.fn((options, callback) => {
return stream;
});

cloudinary.url = '';

module.exports = cloudinary;
4 changes: 1 addition & 3 deletions __mocks__/download.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* eslint-env node, jest */

const download = jest.fn((options) => Promise.resolve(download.response));
const download = vi.fn((options) => Promise.resolve(download.response));
download.get = download;
download.post = download;

Expand Down
39 changes: 27 additions & 12 deletions __mocks__/fs.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/* eslint-env node, jest */

const fs = jest.genMockFromModule('fs');
const realFs = jest.requireActual('fs');
const fs = {};
const realFs = require('fs');
const Path = require('path');
const {PassThrough} = require('stream');

fs.constants = realFs.constants;
fs.virtualFiles = {};

fs.promises = {};

fs.readFile = jest.fn((...args) => {
fs.readFile = vi.fn((...args) => {
const [path, callback] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -21,7 +20,7 @@ fs.readFile = jest.fn((...args) => {
}
});

fs.readFileSync = jest.fn((...args) => {
fs.readFileSync = vi.fn((...args) => {
const [path] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -32,7 +31,7 @@ fs.readFileSync = jest.fn((...args) => {
}
});

fs.promises.readFile = jest.fn((...args) => {
fs.promises.readFile = vi.fn((...args) => {
const [path] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -43,7 +42,14 @@ fs.promises.readFile = jest.fn((...args) => {
}
});

fs.promises.readdir = jest.fn((...args) => {
fs.promises.writeFile = vi.fn((...args) => {
const [file, data] = args;
const fullPath = Path.resolve(process.cwd(), file);
fs.virtualFiles[fullPath] = data;
return new Promise((resolve) => resolve());
});

fs.promises.readdir = vi.fn((...args) => {
const [path] = args;
const fullPath = Path.resolve(process.cwd(), path);
const files = Object.keys(fs.virtualFiles).filter((file) => file.startsWith(fullPath));
Expand All @@ -55,7 +61,7 @@ fs.promises.readdir = jest.fn((...args) => {
}
});

fs.access = jest.fn((...args) => {
fs.access = vi.fn((...args) => {
const [path, , callback] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -67,7 +73,7 @@ fs.access = jest.fn((...args) => {
}
});

fs.accessSync = jest.fn((...args) => {
fs.accessSync = vi.fn((...args) => {
const [path] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -78,7 +84,7 @@ fs.accessSync = jest.fn((...args) => {
}
});

fs.createReadStream = jest.fn((...args) => {
fs.createReadStream = vi.fn((...args) => {
const [path, options] = args;
const fullPath = Path.resolve(process.cwd(), path);

Expand All @@ -93,7 +99,7 @@ fs.createReadStream = jest.fn((...args) => {
}
});

fs.writeFile = jest.fn((file, data, ...rest) => {
fs.writeFile = vi.fn((file, data, ...rest) => {
let options, callback;
if (rest.length === 1) {
callback = rest[0];
Expand All @@ -106,4 +112,13 @@ fs.writeFile = jest.fn((file, data, ...rest) => {
callback(null);
});

fs.mkdir = vi.fn((...args) => {
const [path, options, callback] = args;

// no-op

callback(null);
});

fs.default = fs;
module.exports = fs;
15 changes: 15 additions & 0 deletions __mocks__/scrape-it.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from 'axios';
import type { ScrapeOptions } from 'scrape-it';
import { vi } from 'vitest';

const mockScrapeIt = async (url: string, opts: ScrapeOptions) => {
const scrapeIt = await vi.importActual<typeof import('scrape-it')>('scrape-it');
const cheerio = await vi.importActual<typeof import('cheerio')>('cheerio');
const res = await axios(url);

return {
data: scrapeIt.scrapeHTML(cheerio.load(res.data), opts),
};
};

export default mockScrapeIt;
10 changes: 4 additions & 6 deletions __mocks__/sqlite.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-env node, jest */
const sqlite = {};

const sqlite = jest.genMockFromModule('sqlite');

sqlite.open = jest.fn(() => ({
all: jest.fn(() => Promise.resolve(sqlite.records)),
get: jest.fn(() => Promise.resolve(sqlite.records.length >= 1 ? sqlite.records[0] : null)),
sqlite.open = vi.fn(() => ({
all: vi.fn(() => Promise.resolve(sqlite.records)),
get: vi.fn(() => Promise.resolve(sqlite.records.length >= 1 ? sqlite.records[0] : null)),
}));

sqlite.records = [];
Expand Down
4 changes: 1 addition & 3 deletions __mocks__/tinyreq.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
/* eslint-env node, jest */

const {PassThrough} = require('stream');

const tinyreq = (...args) => (
tinyreq.impl(...args)
);

tinyreq.impl = jest.fn(() => {
tinyreq.impl = vi.fn(() => {
return Promise.resolve(tinyreq.response);
});

Expand Down
4 changes: 1 addition & 3 deletions __mocks__/word2vec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-env node, jest */

class Model {
similarity() {
return 0;
Expand All @@ -10,6 +8,6 @@ class Model {
}
}

module.exports.loadModel = jest.fn((params, done) => {
module.exports.loadModel = vi.fn((params, done) => {
done(null, new Model());
});
3 changes: 2 additions & 1 deletion achievement-quiz/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import achievementQuiz from './index';
import Slack from '../lib/slackMock';

jest.mock('../lib/slackUtils');
vi.mock('../achievements');
vi.mock('../lib/slackUtils');

let slack: Slack;

Expand Down
18 changes: 14 additions & 4 deletions achievements/__mocks__/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
export const unlock = () => {};
export const increment = () => {};
export const get = () => {};
export const set = () => {};
export default async () => {};

export const unlock = (user: string, name: string, additionalInfo?: string) => {
console.log(`[achievements stub] ${user} unlocked ${name}${additionalInfo ? `, ${additionalInfo}` : ''}`);
};
export const isUnlocked = () => false;
export const increment = (user: string, name: string, value: number = 1) => {
console.log(`[achievements stub] ${user} increased ${name} by ${value}`);
};
export const get = (): unknown => null;
export const set = (user: string, name: string, value: unknown) => {
console.log(`[achievements stub] ${user} set ${name} = ${value}`);
};
export const lock = () => {};
2 changes: 2 additions & 0 deletions achievements/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

if (process.env.NODE_ENV === 'production') {
module.exports = require('./index_production');
} else if (process.env.NODE_ENV === 'test') {
module.exports = {default: async () => {}, unlock: () => {}, isUnlocked: () => false, increment: () => {}, get: () => null, set: () => {}, lock: () => {}};
} else {
module.exports = require('./index_development');
}
6 changes: 3 additions & 3 deletions achievements/index_production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import achievements from './index_production';

let slack: Slack = null;

jest.mock('../lib/slackUtils');
vi.mock('../lib/slackUtils');

jest.mock('../lib/state');
vi.mock('../lib/state');

jest.mock('../lib/firestore', () => {
vi.mock('../lib/firestore', () => {
const firebase = new MockFirebase({});
const db = firebase.firestore();
db.runTransaction = noop;
Expand Down
1 change: 0 additions & 1 deletion achievements/index_production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import {Mutex} from 'async-mutex';
import {stripIndent} from 'common-tags';
import type {CollectionReference} from 'firebase-admin/firestore';
// @ts-expect-error: Not typed
import japanese from 'japanese';
import {countBy, throttle, groupBy, get as getter, chunk, uniq} from 'lodash';
import moment from 'moment';
Expand Down Expand Up @@ -205,7 +204,7 @@
return;
}

const newAchievedEmojis = uniq([...achievedEmojis, event.reaction]);

Check warning on line 207 in achievements/index_production.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider Set instead of uniq() from lodash; for example, use `[...new Set(values)]`. Check that the behavior is equivalent because the library handles nullish values differently from the native API.

See more on https://sonarcloud.io/project/issues?id=tsg-ut_slackbot&issues=AZ8_0YIfcDaOUeGsTeAL&open=AZ8_0YIfcDaOUeGsTeAL&pullRequest=1223
await increment(event.user, `reaction-${mode}-reactions`);
await set(event.user, `reaction-${mode}-reactions-emojis`, newAchievedEmojis);
await set(event.user, `reaction-${mode}-reactions-emoji-types`, newAchievedEmojis.length);
Expand Down
4 changes: 2 additions & 2 deletions ahokusa/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ahokusa from './index';
import Slack from '../lib/slackMock';

jest.mock('../achievements');
jest.mock('../lib/slackUtils');
vi.mock('../achievements');
vi.mock('../lib/slackUtils');

let slack: Slack;

Expand Down
26 changes: 26 additions & 0 deletions atequiz/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// XXX: このファイルは mahjong/index.js (CJS) がテスト環境で require('../atequiz/index') でロードするために存在するスタブ。
// mahjong/index.js は CJS 形式のため Vite のモジュール解決をバイパスし、Node の CJS ローダーが
// .ts 拡張子を解決できないので、atequiz/index.ts の代わりにこの .js スタブが必要になっている。
// プロジェクトの ESM 化(または mahjong/index.js の TypeScript 化)完了後は必ずこのファイルを削除すること。(#846)

class AteQuiz {
constructor(slack, problem, options) {
this.problem = problem;
}

start() {
return Promise.resolve({
state: 'unsolved',
quiz: this.problem,
hintIndex: null,
correctAnswerer: null,
});
}
}

const typicalAteQuizHintTexts = [];

const typicalMessageTextsGenerator = {};

module.exports = {AteQuiz, typicalAteQuizHintTexts, typicalMessageTextsGenerator};
module.exports.default = module.exports;
Loading
Loading