Skip to content

Commit 8fd2d75

Browse files
authored
Merge pull request #2 from sjsjsj1246/codex/restore-main-tests
[-]: packages/main 테스트 인프라 복구
2 parents 3a6fd62 + 97a7387 commit 8fd2d75

8 files changed

Lines changed: 332 additions & 7 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: Main Package Coverage Comment
2+
3+
on:
4+
workflow_run:
5+
workflows:
6+
- Main Package Test
7+
types:
8+
- completed
9+
10+
jobs:
11+
comment:
12+
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
13+
runs-on: ubuntu-latest
14+
permissions:
15+
actions: read
16+
contents: read
17+
issues: write
18+
pull-requests: write
19+
20+
steps:
21+
- name: Download coverage artifact from triggering run
22+
uses: actions/github-script@v7
23+
env:
24+
ARTIFACT_NAME: main-package-coverage
25+
ARTIFACT_ZIP: ${{ runner.temp }}/main-package-coverage.zip
26+
with:
27+
script: |
28+
const fs = require('fs');
29+
30+
const runId = context.payload.workflow_run.id;
31+
const artifactName = process.env.ARTIFACT_NAME;
32+
const { data } = await github.rest.actions.listWorkflowRunArtifacts({
33+
owner: context.repo.owner,
34+
repo: context.repo.repo,
35+
run_id: runId,
36+
});
37+
38+
const artifact = data.artifacts.find((item) => item.name === artifactName);
39+
if (!artifact) {
40+
core.setFailed(`Artifact "${artifactName}" was not found for workflow run ${runId}.`);
41+
return;
42+
}
43+
44+
const download = await github.rest.actions.downloadArtifact({
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
artifact_id: artifact.id,
48+
archive_format: 'zip',
49+
});
50+
51+
fs.writeFileSync(process.env.ARTIFACT_ZIP, Buffer.from(download.data));
52+
53+
- name: Extract coverage summary
54+
run: |
55+
mkdir -p "${RUNNER_TEMP}/main-package-coverage"
56+
unzip -o "${RUNNER_TEMP}/main-package-coverage.zip" -d "${RUNNER_TEMP}/main-package-coverage"
57+
58+
- name: Comment coverage on pull request
59+
uses: actions/github-script@v7
60+
env:
61+
COMMENT_MARKER: "<!-- main-package-coverage-comment -->"
62+
COVERAGE_SUMMARY_PATH: ${{ runner.temp }}/main-package-coverage/coverage-summary.json
63+
with:
64+
script: |
65+
const fs = require('fs');
66+
67+
const pr = context.payload.workflow_run.pull_requests[0];
68+
if (!pr) {
69+
core.info('No pull request is associated with this workflow run.');
70+
return;
71+
}
72+
73+
const summaryPath = process.env.COVERAGE_SUMMARY_PATH;
74+
if (!fs.existsSync(summaryPath)) {
75+
core.setFailed(`Coverage summary was not found at ${summaryPath}.`);
76+
return;
77+
}
78+
79+
const { total } = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
80+
const formatMetric = (metric) => `${metric.pct.toFixed(2)}% (${metric.covered}/${metric.total})`;
81+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.payload.workflow_run.id}`;
82+
const body = [
83+
process.env.COMMENT_MARKER,
84+
'## packages/main coverage',
85+
'',
86+
'| Metric | Result |',
87+
'| --- | --- |',
88+
`| Lines | ${formatMetric(total.lines)} |`,
89+
`| Statements | ${formatMetric(total.statements)} |`,
90+
`| Functions | ${formatMetric(total.functions)} |`,
91+
`| Branches | ${formatMetric(total.branches)} |`,
92+
'',
93+
`[Workflow run](${runUrl})`,
94+
].join('\n');
95+
96+
const comments = await github.paginate(github.rest.issues.listComments, {
97+
owner: context.repo.owner,
98+
repo: context.repo.repo,
99+
issue_number: pr.number,
100+
per_page: 100,
101+
});
102+
103+
const existingComment = comments.find((comment) =>
104+
comment.user?.type === 'Bot' && comment.body?.includes(process.env.COMMENT_MARKER)
105+
);
106+
107+
if (existingComment) {
108+
await github.rest.issues.updateComment({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
comment_id: existingComment.id,
112+
body,
113+
});
114+
return;
115+
}
116+
117+
await github.rest.issues.createComment({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
issue_number: pr.number,
121+
body,
122+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Main Package Test
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Install pnpm
20+
uses: pnpm/action-setup@v4
21+
with:
22+
version: 10.12.4
23+
24+
- name: Use Node.js 20
25+
uses: actions/setup-node@v4
26+
with:
27+
node-version: 20
28+
cache: pnpm
29+
30+
- name: Install dependencies
31+
run: pnpm install --frozen-lockfile
32+
33+
- name: Run packages/main tests with coverage
34+
run: pnpm -C packages/main test:coverage
35+
36+
- name: Upload packages/main coverage summary
37+
if: success() && github.event_name == 'pull_request'
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: main-package-coverage
41+
path: packages/main/coverage/coverage-summary.json
42+
if-no-files-found: error
43+
retention-days: 7

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"main": "pnpm -F @react-tutorial-overlay/main",
1010
"document": "pnpm -F @react-tutorial-overlay/document",
1111
"dev": "pnpm -r dev",
12-
"size": "size-limit",
13-
"build": "pnpm size-limit && pnpm main build",
12+
"size": "pnpm main build && size-limit",
13+
"build": "pnpm size",
1414
"docs": "pnpm document build"
1515
},
1616
"keywords": [
@@ -42,4 +42,4 @@
4242
"size-limit": "^7.0.8",
4343
"standard-version": "^9.5.0"
4444
}
45-
}
45+
}

packages/main/jest.config.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1-
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
1+
/** @type {import('jest').Config} */
22
module.exports = {
3-
preset: 'ts-jest',
43
testEnvironment: 'jsdom',
4+
roots: ['<rootDir>/src', '<rootDir>/test'],
5+
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
56
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
7+
moduleFileExtensions: ['ts', 'tsx', 'js'],
8+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
9+
transform: {
10+
'^.+\\.(ts|tsx)$': [
11+
'ts-jest',
12+
{
13+
tsconfig: {
14+
esModuleInterop: true,
15+
jsx: 'react',
16+
lib: ['dom', 'esnext'],
17+
module: 'commonjs',
18+
noEmit: true,
19+
rootDir: '.',
20+
},
21+
},
22+
],
23+
},
624
};

packages/main/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"scripts": {
44
"dev": "tsup --watch",
55
"build": "tsup",
6-
"test": "jest --runInBand"
6+
"test": "jest --runInBand",
7+
"test:coverage": "jest --runInBand --coverage --coverageReporters=text-summary --coverageReporters=json-summary"
78
},
89
"husky": {
910
"hooks": {
@@ -43,4 +44,4 @@
4344
"react": ">=16",
4445
"react-dom": ">=16"
4546
}
46-
}
47+
}

packages/main/test/setup.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import '@testing-library/jest-dom';
2+
import { cleanup, act } from '@testing-library/react';
3+
import { tutorial } from '../src/core/tutorial';
4+
5+
Object.defineProperty(Element.prototype, 'scrollIntoView', {
6+
configurable: true,
7+
value: jest.fn(),
8+
writable: true,
9+
});
10+
11+
afterEach(() => {
12+
cleanup();
13+
act(() => {
14+
tutorial.close();
15+
});
16+
jest.clearAllMocks();
17+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import { act, render, screen } from '@testing-library/react';
3+
import { TutorialOverlay } from '../src/components/tutorial-overlay';
4+
import { tutorial } from '../src/core/tutorial';
5+
6+
describe('TutorialOverlay', () => {
7+
test('stays mounted when a target element cannot be found', () => {
8+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
9+
10+
render(<TutorialOverlay />);
11+
12+
act(() => {
13+
tutorial.open({
14+
steps: [
15+
{
16+
title: 'Missing target',
17+
content: 'Overlay stays open',
18+
targetIds: ['does-not-exist'],
19+
},
20+
],
21+
options: {},
22+
});
23+
});
24+
25+
expect(screen.getByText('Missing target')).toBeInTheDocument();
26+
expect(screen.getByText('Overlay stays open')).toBeInTheDocument();
27+
expect(screen.getByText('1 / 1')).toBeInTheDocument();
28+
expect(errorSpy).toHaveBeenCalledWith('Highlighted element with id does-not-exist was not found.');
29+
});
30+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { act, render, screen } from '@testing-library/react';
3+
import { useTutorialStore } from '../src/core/store';
4+
import { tutorial } from '../src/core/tutorial';
5+
import type { Tutorial } from '../src/core/types';
6+
7+
const twoStepTutorial: Tutorial = {
8+
steps: [
9+
{ title: 'Step 1', targetIds: ['first-target'] },
10+
{ title: 'Step 2', targetIds: ['second-target'] },
11+
],
12+
options: {},
13+
};
14+
15+
function StateProbe() {
16+
const {
17+
index,
18+
open,
19+
tutorial: { steps },
20+
} = useTutorialStore();
21+
22+
return (
23+
<div>
24+
<span data-testid="open">{String(open)}</span>
25+
<span data-testid="index">{String(index)}</span>
26+
<span data-testid="title">{steps[index]?.title ?? 'none'}</span>
27+
</div>
28+
);
29+
}
30+
31+
describe('tutorial core API', () => {
32+
test('tutorial.open opens the tutorial and tutorial.next advances the current step', () => {
33+
render(<StateProbe />);
34+
35+
expect(screen.getByTestId('open')).toHaveTextContent('false');
36+
expect(screen.getByTestId('index')).toHaveTextContent('0');
37+
38+
act(() => {
39+
tutorial.open(twoStepTutorial);
40+
});
41+
42+
expect(screen.getByTestId('open')).toHaveTextContent('true');
43+
expect(screen.getByTestId('index')).toHaveTextContent('0');
44+
expect(screen.getByTestId('title')).toHaveTextContent('Step 1');
45+
46+
act(() => {
47+
tutorial.next();
48+
});
49+
50+
expect(screen.getByTestId('open')).toHaveTextContent('true');
51+
expect(screen.getByTestId('index')).toHaveTextContent('1');
52+
expect(screen.getByTestId('title')).toHaveTextContent('Step 2');
53+
});
54+
55+
test('tutorial.next closes the tutorial on the last step', () => {
56+
render(<StateProbe />);
57+
58+
act(() => {
59+
tutorial.open({
60+
steps: [{ title: 'Only step', targetIds: ['only-target'] }],
61+
options: {},
62+
});
63+
});
64+
65+
expect(screen.getByTestId('open')).toHaveTextContent('true');
66+
expect(screen.getByTestId('title')).toHaveTextContent('Only step');
67+
68+
act(() => {
69+
tutorial.next();
70+
});
71+
72+
expect(screen.getByTestId('open')).toHaveTextContent('false');
73+
expect(screen.getByTestId('index')).toHaveTextContent('0');
74+
expect(screen.getByTestId('title')).toHaveTextContent('none');
75+
});
76+
77+
test('tutorial.close closes an open tutorial', () => {
78+
render(<StateProbe />);
79+
80+
act(() => {
81+
tutorial.open(twoStepTutorial);
82+
});
83+
84+
expect(screen.getByTestId('open')).toHaveTextContent('true');
85+
86+
act(() => {
87+
tutorial.close();
88+
});
89+
90+
expect(screen.getByTestId('open')).toHaveTextContent('false');
91+
expect(screen.getByTestId('index')).toHaveTextContent('0');
92+
expect(screen.getByTestId('title')).toHaveTextContent('none');
93+
});
94+
});

0 commit comments

Comments
 (0)