Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Run Tests

on:
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18.20.8'

- name: Cache Node.js modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Timer criado durante gravação de vídeo no Youtube
# ⏱️ Timer criado durante gravação de vídeo no YouTube

Este é um aplicativo simples de timer que foi usado durante a [gravação deste vídeo](https://www.youtube.com/watch?v=ir8MIBhGbcA).

O layout está [aqui no Figma](https://www.figma.com/design/97maginjN0aHjiQPy3dCDS/%231---Timer?m=auto&t=B8ND36dunZtQcZQe-1).

---

## 🚀 Como executar a aplicação

1. Instale as dependências:
```bash
npm install
```

2. Inicie o ambiente de desenvolvimento:
```bash
npm run dev
```

3. Acesse no navegador:
```
http://localhost:1234
```

---

## 🧪 Como executar os testes

Para rodar os testes automatizados com Jest:

```bash
npm test
```

Ou:

```bash
npm run test
```
2 changes: 2 additions & 0 deletions asset/javascript/TimeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,5 @@ document.addEventListener('DOMContentLoaded', function () {
const timerController = new TimerController(reference);
window.timerController = timerController;
});

export default TimerController;
61 changes: 61 additions & 0 deletions asset/javascript/__mocks__/TimeController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { jest } from '@jest/globals';

// Mock de dependências externas
jest.mock('../../sound/tick-tack.wav', () => 'tick-tack.wav');
jest.mock('../../sound/stop.mp3', () => 'stop.mp3');
jest.mock('../helper.js', () => ({}), { virtual: true });

// Mock do TimerStatus e TimeUtils
jest.mock('../TimerStatus.js', () => ({
TimerStatus: {
STOPPED: 'STOPPED',
PAUSED: 'PAUSED',
COUNTDOWN: 'COUNTDOWN',
RUNNING: 'RUNNING',
EDITING: 'EDITING',
isRunning: jest.fn(),
isCountdown: jest.fn(),
isPaused: jest.fn(),
isStopped: jest.fn(),
}
}));

jest.mock('../TimeUtils.js', () => ({
formatTimeUnit: jest.fn(v => String(v).padStart(2, '0').slice(-2)),
hourToSeconds: jest.fn(h => h * 3600),
minuteToSeconds: jest.fn(m => m * 60),
remainingSeconds: jest.fn(s => s % 60),
secondsToHour: jest.fn(s => Math.floor(s / 3600)),
secondsToMinute: jest.fn(s => Math.floor((s % 3600) / 60)),
}));
function createMockInput() {
return {
value: '00',
addEventListener: jest.fn(),
focus: jest.fn(),
disabled: false,
};
}
function createMockButton() {
return {
addEventListener: jest.fn(),
showElement: jest.fn(),
hideElement: jest.fn(),
focus: jest.fn(),
};
}
function createMockContainer() {
return {
querySelector: jest.fn((selector) => {
if (selector.includes('input')) return createMockInput();
if (selector.includes('button')) return createMockButton();
if (selector.includes('number')) return { textContent: '', classList: { add: jest.fn(), remove: jest.fn() } };
return {};
}),
showElement: jest.fn(),
hideElement: jest.fn(),
classList: { add: jest.fn(), remove: jest.fn() },
};
}

export { createMockInput, createMockButton, createMockContainer };
6 changes: 6 additions & 0 deletions asset/javascript/__mocks__/TimeUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const formatTimeUnit = jest.fn(value => value.toString().padStart(2, '0'));
export const hourToSeconds = jest.fn(hours => hours * 3600);
export const minuteToSeconds = jest.fn(minutes => minutes * 60);
export const remainingSeconds = jest.fn(seconds => seconds % 60);
export const secondsToHour = jest.fn(seconds => Math.floor(seconds / 3600));
export const secondsToMinute = jest.fn(seconds => Math.floor((seconds % 3600) / 60));
8 changes: 8 additions & 0 deletions asset/javascript/__mocks__/audioMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global.Audio = jest.fn().mockImplementation(() => ({
play: jest.fn(),
pause: jest.fn(),
addEventListener: jest.fn(),
volume: 0,
loop: false,
currentTime: 0,
}));
7 changes: 7 additions & 0 deletions asset/javascript/__mocks__/browserApiMocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Object.defineProperty(document, 'fullscreenElement', {
value: null,
writable: true,
});

document.documentElement.requestFullscreen = jest.fn();
document.exitFullscreen = jest.fn();
32 changes: 32 additions & 0 deletions asset/javascript/__mocks__/domMocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const mockElement = (() => {
const cache = {};

return () => ({
querySelector: jest.fn(selector => {
if (!cache[selector]) {
if (String(selector).includes('.js-*-button')) {
cache[selector] = {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
};
} else {
cache[selector] = mockElement();
}
}
return cache[selector];
}),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
focus: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
classList: {
add: jest.fn(),
remove: jest.fn(),
},
value: '00',
textContent: '',
disabled: false,
});
})();
1 change: 1 addition & 0 deletions asset/javascript/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
123 changes: 123 additions & 0 deletions asset/javascript/__tests__/TimeController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import TimerController from '../TimeController';
import * as TimeUtils from '../TimeUtils';

jest.mock('../TimeUtils');

describe('TimerController', () => {
let mockReference;

beforeEach(() => {
mockReference = {
querySelector: jest.fn(selector => {
if (String(selector).includes('input')) {
return {
addEventListener: jest.fn(),
value: '00',
disabled: false,
};
}
if (selector === '.js-stopwatch-action-buttons') {
return {
querySelector: jest.fn(innerSelector => {
if (innerSelector === '.js-start-button') {
return {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
return {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
if (selector === '.js-edit-container-stopwatch') {
return {
querySelector: jest.fn(innerSelector => ({
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
})),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
if (selector === '.js-countdown-container') {
return {
querySelector: jest.fn(innerSelector => {
if (innerSelector === '.js-close-countdown-button') {
return {
addEventListener: jest.fn(),
};
}
return {
textContent: '',
};
}),
hideElement: jest.fn(),
showElement: jest.fn(),
classList: {
add: jest.fn(),
remove: jest.fn(),
},
textContent: '',
};
}
if (selector === '.js-stopwatch-action-buttons') {
return {
querySelector: jest.fn(innerSelector => {
if (innerSelector === '.js-start-button') {
const mockAddEventListener = jest.fn();
return {
addEventListener: mockAddEventListener,
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
if (innerSelector === '.js-pause-button') {
return {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
return {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}),
hideElement: jest.fn(),
showElement: jest.fn(),
};
}
return {
addEventListener: jest.fn(),
hideElement: jest.fn(),
showElement: jest.fn(),
focus: jest.fn(),
value: '00',
classList: { add: jest.fn(), remove: jest.fn() },
};
}),
};
});


it('deve inicializar corretamente', () => {
const timerController = new TimerController(mockReference);
expect(mockReference.querySelector).toHaveBeenCalledWith('.js-hour-input');
});

it('deve formatar corretamente os valores de tempo', () => {
TimeUtils.formatTimeUnit.mockReturnValue('05');
const result = TimeUtils.formatTimeUnit(5);
expect(result).toBe('05');
expect(TimeUtils.formatTimeUnit).toHaveBeenCalledWith(5);
});
});
31 changes: 31 additions & 0 deletions asset/javascript/__tests__/TimeStatus.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TimerStatus } from '../TimerStatus';

describe('TimerStatus', () => {
test('should have correct status values', () => {
expect(TimerStatus.STOPPED).toBe('STOPPED');
expect(TimerStatus.PAUSED).toBe('PAUSED');
expect(TimerStatus.COUNTDOWN).toBe('COUNTDOWN');
expect(TimerStatus.RUNNING).toBe('RUNNING');
expect(TimerStatus.EDITING).toBe('EDITING');
});

test('isRunning returns true only for RUNNING', () => {
expect(TimerStatus.isRunning('RUNNING')).toBe(true);
expect(TimerStatus.isRunning('PAUSED')).toBe(false);
});

test('isCountdown returns true only for COUNTDOWN', () => {
expect(TimerStatus.isCountdown('COUNTDOWN')).toBe(true);
expect(TimerStatus.isCountdown('RUNNING')).toBe(false);
});

test('isPaused returns true only for PAUSED', () => {
expect(TimerStatus.isPaused('PAUSED')).toBe(true);
expect(TimerStatus.isPaused('STOPPED')).toBe(false);
});

test('isStopped returns true only for STOPPED', () => {
expect(TimerStatus.isStopped('STOPPED')).toBe(true);
expect(TimerStatus.isStopped('EDITING')).toBe(false);
});
});
Loading