-
Notifications
You must be signed in to change notification settings - Fork 4
test: 补充 zip-options / i18n / user / plugin-config 测试 #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+448
−0
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { describe, expect, test } from 'bun:test'; | ||
| import i18next from 'i18next'; | ||
| import { t } from '../src/utils/i18n'; | ||
|
|
||
| describe('i18n t()', () => { | ||
| test('returns a non-empty translated string for a known key in English', async () => { | ||
| await i18next.changeLanguage('en'); | ||
| const result = t('cancelled'); | ||
| expect(typeof result).toBe('string'); | ||
| expect(result.length).toBeGreaterThan(0); | ||
| expect(result).toBe('Cancelled'); | ||
| }); | ||
|
|
||
| test('returns a non-empty translated string for a known key in Chinese', () => { | ||
| i18next.changeLanguage('zh'); | ||
| const result = t('cancelled'); | ||
| expect(typeof result).toBe('string'); | ||
| expect(result.length).toBeGreaterThan(0); | ||
| expect(result).toBe('已取消'); | ||
| }); | ||
|
|
||
| test('returns a translated string for a key with interpolation in English', () => { | ||
| i18next.changeLanguage('en'); | ||
| const result = t('createAppSuccess', { id: '12345' }); | ||
| expect(typeof result).toBe('string'); | ||
| expect(result).toContain('12345'); | ||
| }); | ||
|
|
||
| test('returns a translated string for a key with interpolation in Chinese', () => { | ||
| i18next.changeLanguage('zh'); | ||
| const result = t('createAppSuccess', { id: '67890' }); | ||
| expect(typeof result).toBe('string'); | ||
| expect(result).toContain('67890'); | ||
| }); | ||
|
|
||
| test('handles multiple interpolation options', () => { | ||
| i18next.changeLanguage('en'); | ||
| const result = t('versionBind', { | ||
| version: '1.0.0', | ||
| nativeVersion: '2.0', | ||
| id: 'abc', | ||
| }); | ||
| expect(result).toContain('1.0.0'); | ||
| expect(result).toContain('2.0'); | ||
| expect(result).toContain('abc'); | ||
| }); | ||
|
|
||
| test('returns the key itself or a fallback for an unknown key', async () => { | ||
| await i18next.changeLanguage('en'); | ||
| const result = t('this_key_does_not_exist_at_all'); | ||
| // i18next returns the key string when a key is missing | ||
| expect(result).toBe('this_key_does_not_exist_at_all'); | ||
| }); | ||
|
|
||
| test('returns different strings for en and zh for the same key', () => { | ||
| i18next.changeLanguage('en'); | ||
| const enResult = t('packing'); | ||
| i18next.changeLanguage('zh'); | ||
| const zhResult = t('packing'); | ||
| // Both should be non-empty strings | ||
| expect(enResult.length).toBeGreaterThan(0); | ||
| expect(zhResult.length).toBeGreaterThan(0); | ||
| // They should differ (different languages) | ||
| expect(enResult).not.toBe(zhResult); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; | ||
| import fs from 'fs-extra'; | ||
| import os from 'os'; | ||
| import path from 'path'; | ||
|
|
||
| import { plugins } from '../src/utils/plugin-config'; | ||
|
|
||
| describe('plugin-config - sentry plugin', () => { | ||
| let tmpDir: string; | ||
|
|
||
| beforeEach(async () => { | ||
| tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plugin-config-test-')); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await fs.remove(tmpDir); | ||
| }); | ||
|
|
||
| const sentryPlugin = plugins.find((p) => p.name === 'sentry'); | ||
|
|
||
| test('sentry plugin exists in the plugins array', () => { | ||
| expect(sentryPlugin).toBeDefined(); | ||
| }); | ||
|
|
||
| test('sentry bundleParams are { sentry: true, sourcemap: true }', () => { | ||
| expect(sentryPlugin?.bundleParams).toEqual({ | ||
| sentry: true, | ||
| sourcemap: true, | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentry detect', () => { | ||
| test('returns false when no sentry.properties exists', async () => { | ||
| const origCwd = process.cwd(); | ||
| process.chdir(tmpDir); | ||
| try { | ||
| const result = await sentryPlugin?.detect(); | ||
| expect(result).toBe(false); | ||
| } finally { | ||
| process.chdir(origCwd); | ||
| } | ||
| }); | ||
|
|
||
| test('returns true when ios/sentry.properties exists', async () => { | ||
| await fs.ensureDir(path.join(tmpDir, 'ios')); | ||
| await fs.writeFile( | ||
| path.join(tmpDir, 'ios', 'sentry.properties'), | ||
| 'defaults.org=test\n', | ||
| ); | ||
|
|
||
| const origCwd = process.cwd(); | ||
| process.chdir(tmpDir); | ||
| try { | ||
| const result = await sentryPlugin?.detect(); | ||
| expect(result).toBe(true); | ||
| } finally { | ||
| process.chdir(origCwd); | ||
| } | ||
| }); | ||
|
|
||
| test('returns true when android/sentry.properties exists', async () => { | ||
| await fs.ensureDir(path.join(tmpDir, 'android')); | ||
| await fs.writeFile( | ||
| path.join(tmpDir, 'android', 'sentry.properties'), | ||
| 'defaults.org=test\n', | ||
| ); | ||
|
|
||
| const origCwd = process.cwd(); | ||
| process.chdir(tmpDir); | ||
| try { | ||
| const result = await sentryPlugin?.detect(); | ||
| expect(result).toBe(true); | ||
| } finally { | ||
| process.chdir(origCwd); | ||
| } | ||
| }); | ||
|
|
||
| test('returns true when both ios and android sentry.properties exist', async () => { | ||
| await fs.ensureDir(path.join(tmpDir, 'ios')); | ||
| await fs.ensureDir(path.join(tmpDir, 'android')); | ||
| await fs.writeFile( | ||
| path.join(tmpDir, 'ios', 'sentry.properties'), | ||
| 'defaults.org=test\n', | ||
| ); | ||
| await fs.writeFile( | ||
| path.join(tmpDir, 'android', 'sentry.properties'), | ||
| 'defaults.org=test\n', | ||
| ); | ||
|
|
||
| const origCwd = process.cwd(); | ||
| process.chdir(tmpDir); | ||
| try { | ||
| const result = await sentryPlugin?.detect(); | ||
| expect(result).toBe(true); | ||
| } finally { | ||
| process.chdir(origCwd); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'; | ||
| import crypto from 'crypto'; | ||
|
|
||
| import * as api from '../src/api'; | ||
| import { userCommands } from '../src/user'; | ||
| import * as utils from '../src/utils'; | ||
|
|
||
| function md5(str: string) { | ||
| return crypto.createHash('md5').update(str).digest('hex'); | ||
| } | ||
|
|
||
| describe('userCommands.login', () => { | ||
| let consoleSpy: ReturnType<typeof spyOn>; | ||
| let postSpy: ReturnType<typeof spyOn>; | ||
| let replaceSessionSpy: ReturnType<typeof spyOn>; | ||
| let saveSessionSpy: ReturnType<typeof spyOn>; | ||
| let questionSpy: ReturnType<typeof spyOn>; | ||
|
|
||
| beforeEach(() => { | ||
| consoleSpy = spyOn(console, 'log').mockImplementation(() => {}); | ||
| postSpy = spyOn(api, 'post').mockResolvedValue({ | ||
| token: 'session-token-abc', | ||
| info: { name: 'TestUser', email: 'test@example.com' }, | ||
| }); | ||
| replaceSessionSpy = spyOn(api, 'replaceSession').mockImplementation( | ||
| () => {}, | ||
| ); | ||
| saveSessionSpy = spyOn(api, 'saveSession').mockResolvedValue(undefined); | ||
| questionSpy = spyOn(utils, 'question').mockResolvedValue('fallback'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| consoleSpy.mockRestore(); | ||
| postSpy.mockRestore(); | ||
| replaceSessionSpy.mockRestore(); | ||
| saveSessionSpy.mockRestore(); | ||
| questionSpy.mockRestore(); | ||
| }); | ||
|
|
||
| test('calls post with /user/login and md5-hashes the password', async () => { | ||
| await userCommands.login({ | ||
| args: ['user@example.com', 'mypassword'], | ||
| }); | ||
|
|
||
| expect(postSpy).toHaveBeenCalledWith('/user/login', { | ||
| email: 'user@example.com', | ||
| pwd: md5('mypassword'), | ||
| }); | ||
| }); | ||
|
|
||
| test('md5 hash is a valid 32-char hex string', async () => { | ||
| await userCommands.login({ | ||
| args: ['user@example.com', 'secret123'], | ||
| }); | ||
|
|
||
| const callArgs = postSpy.mock.calls[0]; | ||
| const pwdHash = callArgs[1].pwd as string; | ||
| expect(pwdHash).toHaveLength(32); | ||
| expect(pwdHash).toMatch(/^[0-9a-f]{32}$/); | ||
| expect(pwdHash).toBe(md5('secret123')); | ||
| }); | ||
|
|
||
| test('calls replaceSession with the returned token', async () => { | ||
| await userCommands.login({ | ||
| args: ['user@example.com', 'mypassword'], | ||
| }); | ||
|
|
||
| expect(replaceSessionSpy).toHaveBeenCalledWith({ | ||
| token: 'session-token-abc', | ||
| }); | ||
| }); | ||
|
|
||
| test('calls saveSession after replaceSession', async () => { | ||
| await userCommands.login({ | ||
| args: ['user@example.com', 'mypassword'], | ||
| }); | ||
|
|
||
| expect(saveSessionSpy).toHaveBeenCalled(); | ||
| expect(replaceSessionSpy).toHaveBeenCalled(); | ||
|
|
||
| // Verify call order: replaceSession should be called before saveSession | ||
| const replaceOrder = replaceSessionSpy.mock.invocationCallOrder[0]; | ||
| const saveOrder = saveSessionSpy.mock.invocationCallOrder[0]; | ||
| expect(replaceOrder).toBeLessThan(saveOrder); | ||
| }); | ||
|
|
||
| test('prompts for email and password when args are missing', async () => { | ||
| let _callCount = 0; | ||
| questionSpy.mockImplementation(async (prompt: string) => { | ||
| _callCount++; | ||
| if (prompt === 'email:') return 'asked@email.com'; | ||
| return 'asked-password'; | ||
| }); | ||
|
|
||
| await userCommands.login({ args: [] }); | ||
|
|
||
| expect(questionSpy).toHaveBeenCalledTimes(2); | ||
| expect(postSpy).toHaveBeenCalledWith('/user/login', { | ||
| email: 'asked@email.com', | ||
| pwd: md5('asked-password'), | ||
| }); | ||
| }); | ||
|
|
||
| test('logs welcome message with user name', async () => { | ||
| await userCommands.login({ | ||
| args: ['user@example.com', 'mypassword'], | ||
| }); | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalled(); | ||
| // The welcome message includes the user's name | ||
| const logOutput = consoleSpy.mock.calls[0][0] as string; | ||
| expect(logOutput).toContain('TestUser'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('userCommands.logout', () => { | ||
| let consoleSpy: ReturnType<typeof spyOn>; | ||
| let closeSessionSpy: ReturnType<typeof spyOn>; | ||
|
|
||
| beforeEach(() => { | ||
| consoleSpy = spyOn(console, 'log').mockImplementation(() => {}); | ||
| closeSessionSpy = spyOn(api, 'closeSession').mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| consoleSpy.mockRestore(); | ||
| closeSessionSpy.mockRestore(); | ||
| }); | ||
|
|
||
| test('calls closeSession', async () => { | ||
| await userCommands.logout({} as any); | ||
|
|
||
| expect(closeSessionSpy).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test('logs a message after logout', async () => { | ||
| await userCommands.logout({} as any); | ||
|
|
||
| expect(consoleSpy).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('userCommands.me', () => { | ||
| let consoleSpy: ReturnType<typeof spyOn>; | ||
| let getSpy: ReturnType<typeof spyOn>; | ||
|
|
||
| beforeEach(() => { | ||
| consoleSpy = spyOn(console, 'log').mockImplementation(() => {}); | ||
| getSpy = spyOn(api, 'get').mockResolvedValue({ | ||
| ok: true, | ||
| name: 'TestUser', | ||
| email: 'test@example.com', | ||
| id: '12345', | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| consoleSpy.mockRestore(); | ||
| getSpy.mockRestore(); | ||
| }); | ||
|
|
||
| test('calls get with /user/me', async () => { | ||
| await userCommands.me(); | ||
|
|
||
| expect(getSpy).toHaveBeenCalledWith('/user/me'); | ||
| }); | ||
|
|
||
| test('logs each field except "ok"', async () => { | ||
| await userCommands.me(); | ||
|
|
||
| // Should log name, email, id but NOT ok | ||
| const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string); | ||
| expect(logCalls).toContain('name: TestUser'); | ||
| expect(logCalls).toContain('email: test@example.com'); | ||
| expect(logCalls).toContain('id: 12345'); | ||
|
|
||
| // Should not log the "ok" field | ||
| const hasOk = logCalls.some((msg) => msg.startsWith('ok:')); | ||
| expect(hasOk).toBe(false); | ||
| }); | ||
|
|
||
| test('skips the "ok" field when logging', async () => { | ||
| await userCommands.me(); | ||
|
|
||
| const logCalls = consoleSpy.mock.calls.map((c) => c[0] as string); | ||
| for (const msg of logCalls) { | ||
| expect(msg).not.toMatch(/^ok:/); | ||
| } | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.