ace-interview-prep 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/commands/add.js +92 -0
- package/dist/commands/feedback.js +133 -0
- package/dist/commands/generate.js +224 -0
- package/dist/commands/init.js +100 -0
- package/dist/commands/list.js +107 -0
- package/dist/commands/reset.js +68 -0
- package/dist/commands/score.js +70 -0
- package/dist/commands/setup.js +84 -0
- package/dist/commands/test.js +85 -0
- package/dist/index.js +72 -0
- package/dist/lib/categories.js +103 -0
- package/dist/lib/config.js +61 -0
- package/dist/lib/llm.js +134 -0
- package/dist/lib/paths.js +38 -0
- package/dist/lib/scaffold.js +110 -0
- package/dist/lib/scorecard.js +116 -0
- package/dist/prompts/code-review.md +59 -0
- package/dist/prompts/design-review.md +67 -0
- package/dist/prompts/question-brainstorm.md +31 -0
- package/dist/prompts/question-generate.md +65 -0
- package/dist/templates/design/notes.md.hbs +27 -0
- package/dist/templates/js-ts/solution.test.ts.hbs +11 -0
- package/dist/templates/js-ts/solution.ts.hbs +11 -0
- package/dist/templates/leetcode-algo/solution.test.ts.hbs +11 -0
- package/dist/templates/leetcode-algo/solution.ts.hbs +11 -0
- package/dist/templates/leetcode-ds/solution.test.ts.hbs +11 -0
- package/dist/templates/leetcode-ds/solution.ts.hbs +11 -0
- package/dist/templates/react-apps/App.test.tsx.hbs +16 -0
- package/dist/templates/react-apps/App.tsx.hbs +16 -0
- package/dist/templates/readme.md.hbs +9 -0
- package/dist/templates/web-components/component.test.ts.hbs +11 -0
- package/dist/templates/web-components/component.ts.hbs +22 -0
- package/dist/templates/web-components/index.html.hbs +12 -0
- package/package.json +72 -0
- package/questions/design-be/url-shortener/README.md +23 -0
- package/questions/design-be/url-shortener/notes.md +27 -0
- package/questions/design-be/url-shortener/scorecard.json +1 -0
- package/questions/design-fe/news-feed/README.md +22 -0
- package/questions/design-fe/news-feed/notes.md +27 -0
- package/questions/design-fe/news-feed/scorecard.json +1 -0
- package/questions/design-full/google-docs/README.md +22 -0
- package/questions/design-full/google-docs/notes.md +27 -0
- package/questions/design-full/google-docs/scorecard.json +1 -0
- package/questions/js-ts/debounce/README.md +86 -0
- package/questions/js-ts/debounce/scorecard.json +9 -0
- package/questions/js-ts/debounce/solution.test.ts +128 -0
- package/questions/js-ts/debounce/solution.ts +4 -0
- package/questions/leetcode-algo/two-sum/README.md +58 -0
- package/questions/leetcode-algo/two-sum/scorecard.json +1 -0
- package/questions/leetcode-algo/two-sum/solution.test.ts +55 -0
- package/questions/leetcode-algo/two-sum/solution.ts +4 -0
- package/questions/leetcode-ds/lru-cache/README.md +70 -0
- package/questions/leetcode-ds/lru-cache/scorecard.json +1 -0
- package/questions/leetcode-ds/lru-cache/solution.test.ts +82 -0
- package/questions/leetcode-ds/lru-cache/solution.ts +14 -0
- package/questions/react-apps/todo-app/App.test.tsx +130 -0
- package/questions/react-apps/todo-app/App.tsx +10 -0
- package/questions/react-apps/todo-app/README.md +23 -0
- package/questions/react-apps/todo-app/scorecard.json +9 -0
- package/questions/web-components/star-rating/README.md +45 -0
- package/questions/web-components/star-rating/component.test.ts +64 -0
- package/questions/web-components/star-rating/component.ts +28 -0
- package/questions/web-components/star-rating/index.html +14 -0
- package/questions/web-components/star-rating/scorecard.json +9 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { twoSum } from './solution';
|
|
3
|
+
|
|
4
|
+
describe('twoSum', () => {
|
|
5
|
+
it('basic case: [2,7,11,15] target 9 returns [0,1]', () => {
|
|
6
|
+
const nums = [2, 7, 11, 15];
|
|
7
|
+
const result = twoSum(nums, 9);
|
|
8
|
+
expect(result).toHaveLength(2);
|
|
9
|
+
expect(result.sort()).toEqual([0, 1]);
|
|
10
|
+
expect(nums[result[0]] + nums[result[1]]).toBe(9);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('handles negative numbers', () => {
|
|
14
|
+
const result = twoSum([-1, -2, -3, -4, -5], -8);
|
|
15
|
+
expect(result).toHaveLength(2);
|
|
16
|
+
const [i, j] = result;
|
|
17
|
+
expect([-1, -2, -3, -4, -5][i] + [-1, -2, -3, -4, -5][j]).toBe(-8);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('handles zero in array', () => {
|
|
21
|
+
const result = twoSum([0, 4, 3, 0], 0);
|
|
22
|
+
expect(result).toHaveLength(2);
|
|
23
|
+
const [i, j] = result;
|
|
24
|
+
expect(i).not.toBe(j);
|
|
25
|
+
expect([0, 4, 3, 0][i] + [0, 4, 3, 0][j]).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles large array', () => {
|
|
29
|
+
const nums = Array.from({ length: 10000 }, (_, i) => i);
|
|
30
|
+
const target = 9998;
|
|
31
|
+
const result = twoSum(nums, target);
|
|
32
|
+
expect(result).toHaveLength(2);
|
|
33
|
+
expect(nums[result[0]] + nums[result[1]]).toBe(target);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles duplicate values', () => {
|
|
37
|
+
const result = twoSum([3, 3], 6);
|
|
38
|
+
expect(result).toHaveLength(2);
|
|
39
|
+
expect(result).toContain(0);
|
|
40
|
+
expect(result).toContain(1);
|
|
41
|
+
expect(result[0]).not.toBe(result[1]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('adjacent elements sum to target', () => {
|
|
45
|
+
const result = twoSum([1, 2, 3, 4], 7);
|
|
46
|
+
expect(result).toHaveLength(2);
|
|
47
|
+
expect([1, 2, 3, 4][result[0]] + [1, 2, 3, 4][result[1]]).toBe(7);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('single pair in longer array', () => {
|
|
51
|
+
const result = twoSum([1, 2, 3, 4, 5, 6, 7, 8, 9], 17);
|
|
52
|
+
expect(result).toHaveLength(2);
|
|
53
|
+
expect([1, 2, 3, 4, 5, 6, 7, 8, 9][result[0]] + [1, 2, 3, 4, 5, 6, 7, 8, 9][result[1]]).toBe(17);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Implement an LRU Cache
|
|
2
|
+
|
|
3
|
+
**Category:** LeetCode Data Structures
|
|
4
|
+
**Difficulty:** Hard
|
|
5
|
+
**Suggested Time:** ~45 minutes
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
Implement an LRU (Least Recently Used) Cache class with `get(key)` and `put(key, value)` methods. Both operations must run in **O(1)** time complexity.
|
|
12
|
+
|
|
13
|
+
The constructor takes a `capacity` parameter that determines the maximum number of key-value pairs the cache can hold. When the cache reaches capacity and a new item is inserted, the least recently used item must be evicted.
|
|
14
|
+
|
|
15
|
+
## Class Signature
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
class LRUCache {
|
|
19
|
+
constructor(capacity: number)
|
|
20
|
+
get(key: number): number
|
|
21
|
+
put(key: number, value: number): void
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **`constructor(capacity)`** — Initialize the cache with a maximum capacity.
|
|
26
|
+
- **`get(key)`** — Return the value for the key if it exists, otherwise return `-1`. Accessing a key updates its recency (makes it "most recently used").
|
|
27
|
+
- **`put(key, value)`** — Insert or update the value. If the key already exists, update its value and recency. If the cache is at capacity, evict the least recently used item before inserting.
|
|
28
|
+
|
|
29
|
+
## Examples
|
|
30
|
+
|
|
31
|
+
### Example 1
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const cache = new LRUCache(2);
|
|
35
|
+
|
|
36
|
+
cache.put(1, 1);
|
|
37
|
+
cache.put(2, 2);
|
|
38
|
+
cache.get(1); // returns 1
|
|
39
|
+
cache.put(3, 3); // evicts key 2
|
|
40
|
+
cache.get(2); // returns -1 (not found)
|
|
41
|
+
cache.put(4, 4); // evicts key 1
|
|
42
|
+
cache.get(1); // returns -1 (not found)
|
|
43
|
+
cache.get(3); // returns 3
|
|
44
|
+
cache.get(4); // returns 4
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Example 2
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const cache = new LRUCache(1);
|
|
51
|
+
|
|
52
|
+
cache.put(1, 1);
|
|
53
|
+
cache.get(1); // returns 1
|
|
54
|
+
cache.put(2, 2); // evicts key 1
|
|
55
|
+
cache.get(1); // returns -1
|
|
56
|
+
cache.get(2); // returns 2
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Constraints
|
|
60
|
+
|
|
61
|
+
- `1 <= capacity <= 3000`
|
|
62
|
+
- `0 <= key <= 10^4`
|
|
63
|
+
- `0 <= value <= 10^5`
|
|
64
|
+
- At most `2 * 10^5` calls will be made to `get` and `put`.
|
|
65
|
+
|
|
66
|
+
## Hints
|
|
67
|
+
|
|
68
|
+
- A hash map gives O(1) lookup, but doesn't track order.
|
|
69
|
+
- A doubly linked list gives O(1) add/remove at head or tail.
|
|
70
|
+
- Combine both: map for O(1) lookup, doubly linked list for O(1) recency updates.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"title":"Implement an LRU Cache","category":"leetcode-ds","difficulty":"hard","suggestedTime":45,"status":"untouched","attempts":[],"llmFeedback":null}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LRUCache } from './solution';
|
|
3
|
+
|
|
4
|
+
describe('LRUCache', () => {
|
|
5
|
+
it('basic get and put', () => {
|
|
6
|
+
const cache = new LRUCache(2);
|
|
7
|
+
cache.put(1, 1);
|
|
8
|
+
cache.put(2, 2);
|
|
9
|
+
expect(cache.get(1)).toBe(1);
|
|
10
|
+
expect(cache.get(2)).toBe(2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('evicts least recently used when full', () => {
|
|
14
|
+
const cache = new LRUCache(2);
|
|
15
|
+
cache.put(1, 1);
|
|
16
|
+
cache.put(2, 2);
|
|
17
|
+
cache.put(3, 3); // evicts key 1
|
|
18
|
+
expect(cache.get(1)).toBe(-1);
|
|
19
|
+
expect(cache.get(2)).toBe(2);
|
|
20
|
+
expect(cache.get(3)).toBe(3);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('get updates recency (accessed key is not evicted next)', () => {
|
|
24
|
+
const cache = new LRUCache(2);
|
|
25
|
+
cache.put(1, 1);
|
|
26
|
+
cache.put(2, 2);
|
|
27
|
+
cache.get(1); // makes 1 most recently used
|
|
28
|
+
cache.put(3, 3); // evicts key 2, not 1
|
|
29
|
+
expect(cache.get(1)).toBe(1);
|
|
30
|
+
expect(cache.get(2)).toBe(-1);
|
|
31
|
+
expect(cache.get(3)).toBe(3);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('put existing key updates value and recency', () => {
|
|
35
|
+
const cache = new LRUCache(2);
|
|
36
|
+
cache.put(1, 1);
|
|
37
|
+
cache.put(2, 2);
|
|
38
|
+
cache.put(1, 10); // update value, 1 becomes most recently used
|
|
39
|
+
expect(cache.get(1)).toBe(10);
|
|
40
|
+
cache.put(3, 3); // evicts key 2
|
|
41
|
+
expect(cache.get(1)).toBe(10);
|
|
42
|
+
expect(cache.get(2)).toBe(-1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('capacity of 1', () => {
|
|
46
|
+
const cache = new LRUCache(1);
|
|
47
|
+
cache.put(1, 1);
|
|
48
|
+
expect(cache.get(1)).toBe(1);
|
|
49
|
+
cache.put(2, 2);
|
|
50
|
+
expect(cache.get(1)).toBe(-1);
|
|
51
|
+
expect(cache.get(2)).toBe(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('accessing a key prevents its eviction', () => {
|
|
55
|
+
const cache = new LRUCache(2);
|
|
56
|
+
cache.put(1, 1);
|
|
57
|
+
cache.put(2, 2);
|
|
58
|
+
cache.get(2); // 2 is now most recently used
|
|
59
|
+
cache.put(3, 3); // evicts 1
|
|
60
|
+
expect(cache.get(1)).toBe(-1);
|
|
61
|
+
expect(cache.get(2)).toBe(2);
|
|
62
|
+
expect(cache.get(3)).toBe(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('multiple evictions in sequence', () => {
|
|
66
|
+
const cache = new LRUCache(2);
|
|
67
|
+
cache.put(1, 1);
|
|
68
|
+
cache.put(2, 2);
|
|
69
|
+
cache.put(3, 3); // evicts 1
|
|
70
|
+
cache.put(4, 4); // evicts 2
|
|
71
|
+
expect(cache.get(1)).toBe(-1);
|
|
72
|
+
expect(cache.get(2)).toBe(-1);
|
|
73
|
+
expect(cache.get(3)).toBe(3);
|
|
74
|
+
expect(cache.get(4)).toBe(4);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('empty cache returns -1 for get', () => {
|
|
78
|
+
const cache = new LRUCache(2);
|
|
79
|
+
expect(cache.get(1)).toBe(-1);
|
|
80
|
+
expect(cache.get(99)).toBe(-1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import App from './App';
|
|
4
|
+
|
|
5
|
+
function getInput() {
|
|
6
|
+
return (
|
|
7
|
+
screen.queryByPlaceholderText(/add|what needs|new todo|todo/i) ??
|
|
8
|
+
screen.getByRole('textbox')
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getAddButton() {
|
|
13
|
+
return screen.getByRole('button', { name: /add|submit/i });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('Todo App', () => {
|
|
17
|
+
it('renders heading', () => {
|
|
18
|
+
render(<App />);
|
|
19
|
+
expect(screen.getByRole('heading', { name: /todo app/i })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('can add a todo', () => {
|
|
23
|
+
render(<App />);
|
|
24
|
+
const input = getInput();
|
|
25
|
+
const addButton = getAddButton();
|
|
26
|
+
fireEvent.change(input, { target: { value: 'Buy milk' } });
|
|
27
|
+
fireEvent.click(addButton);
|
|
28
|
+
expect(screen.getByText('Buy milk')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('displays added todo', () => {
|
|
32
|
+
render(<App />);
|
|
33
|
+
const input = getInput();
|
|
34
|
+
const addButton = getAddButton();
|
|
35
|
+
fireEvent.change(input, { target: { value: 'Walk the dog' } });
|
|
36
|
+
fireEvent.click(addButton);
|
|
37
|
+
expect(screen.getByText('Walk the dog')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('can toggle todo complete', () => {
|
|
41
|
+
render(<App />);
|
|
42
|
+
const input = getInput();
|
|
43
|
+
const addButton = getAddButton();
|
|
44
|
+
fireEvent.change(input, { target: { value: 'Test todo' } });
|
|
45
|
+
fireEvent.click(addButton);
|
|
46
|
+
const checkbox =
|
|
47
|
+
screen.getByText('Test todo').closest('li')?.querySelector('input[type="checkbox"]') ??
|
|
48
|
+
screen.getByRole('checkbox');
|
|
49
|
+
fireEvent.click(checkbox);
|
|
50
|
+
expect(checkbox).toBeChecked();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('can delete a todo', () => {
|
|
54
|
+
render(<App />);
|
|
55
|
+
const input = getInput();
|
|
56
|
+
const addButton = getAddButton();
|
|
57
|
+
fireEvent.change(input, { target: { value: 'Delete me' } });
|
|
58
|
+
fireEvent.click(addButton);
|
|
59
|
+
expect(screen.getByText('Delete me')).toBeInTheDocument();
|
|
60
|
+
const deleteButton =
|
|
61
|
+
screen.queryByRole('button', { name: /delete|remove|x/i }) ??
|
|
62
|
+
screen.queryByLabelText(/delete|remove/i);
|
|
63
|
+
if (!deleteButton) throw new Error('Delete button not found');
|
|
64
|
+
fireEvent.click(deleteButton);
|
|
65
|
+
expect(screen.queryByText('Delete me')).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('filter shows only active todos', () => {
|
|
69
|
+
render(<App />);
|
|
70
|
+
const input = getInput();
|
|
71
|
+
const addButton = getAddButton();
|
|
72
|
+
fireEvent.change(input, { target: { value: 'Active todo' } });
|
|
73
|
+
fireEvent.click(addButton);
|
|
74
|
+
fireEvent.change(input, { target: { value: 'Completed todo' } });
|
|
75
|
+
fireEvent.click(addButton);
|
|
76
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
77
|
+
fireEvent.click(checkboxes[1]);
|
|
78
|
+
const activeFilter =
|
|
79
|
+
screen.queryByRole('button', { name: /active/i }) ?? screen.queryByText(/active/i);
|
|
80
|
+
if (!activeFilter) throw new Error('Active filter not found');
|
|
81
|
+
fireEvent.click(activeFilter);
|
|
82
|
+
expect(screen.getByText('Active todo')).toBeInTheDocument();
|
|
83
|
+
expect(screen.queryByText('Completed todo')).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('filter shows only completed todos', () => {
|
|
87
|
+
render(<App />);
|
|
88
|
+
const input = getInput();
|
|
89
|
+
const addButton = getAddButton();
|
|
90
|
+
fireEvent.change(input, { target: { value: 'Todo one' } });
|
|
91
|
+
fireEvent.click(addButton);
|
|
92
|
+
fireEvent.change(input, { target: { value: 'Todo two' } });
|
|
93
|
+
fireEvent.click(addButton);
|
|
94
|
+
const checkboxes = screen.getAllByRole('checkbox');
|
|
95
|
+
fireEvent.click(checkboxes[0]);
|
|
96
|
+
const completedFilter =
|
|
97
|
+
screen.queryByRole('button', { name: /completed/i }) ?? screen.queryByText(/completed/i);
|
|
98
|
+
if (!completedFilter) throw new Error('Completed filter not found');
|
|
99
|
+
fireEvent.click(completedFilter);
|
|
100
|
+
expect(screen.getByText('Todo one')).toBeInTheDocument();
|
|
101
|
+
expect(screen.queryByText('Todo two')).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('shows remaining count', () => {
|
|
105
|
+
render(<App />);
|
|
106
|
+
const input = getInput();
|
|
107
|
+
const addButton = getAddButton();
|
|
108
|
+
fireEvent.change(input, { target: { value: 'First' } });
|
|
109
|
+
fireEvent.click(addButton);
|
|
110
|
+
fireEvent.change(input, { target: { value: 'Second' } });
|
|
111
|
+
fireEvent.click(addButton);
|
|
112
|
+
const countEl =
|
|
113
|
+
screen.queryByText(/2.*(item|remaining|left)/i) ?? screen.queryByText(/^2$/);
|
|
114
|
+
expect(countEl).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('can add multiple todos', () => {
|
|
118
|
+
render(<App />);
|
|
119
|
+
const input = getInput();
|
|
120
|
+
const addButton = getAddButton();
|
|
121
|
+
const todos = ['First todo', 'Second todo', 'Third todo'];
|
|
122
|
+
todos.forEach((todo) => {
|
|
123
|
+
fireEvent.change(input, { target: { value: todo } });
|
|
124
|
+
fireEvent.click(addButton);
|
|
125
|
+
});
|
|
126
|
+
todos.forEach((todo) => {
|
|
127
|
+
expect(screen.getByText(todo)).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Build a Todo App with Filters
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Build a React Todo app with the following features:
|
|
6
|
+
|
|
7
|
+
- Add todos
|
|
8
|
+
- Toggle complete
|
|
9
|
+
- Delete todos
|
|
10
|
+
- Filter by All / Active / Completed
|
|
11
|
+
- Show count of remaining items
|
|
12
|
+
|
|
13
|
+
## Category
|
|
14
|
+
|
|
15
|
+
React Web Apps
|
|
16
|
+
|
|
17
|
+
## Difficulty
|
|
18
|
+
|
|
19
|
+
Medium
|
|
20
|
+
|
|
21
|
+
## Suggested Time
|
|
22
|
+
|
|
23
|
+
~45 minutes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Build a Star Rating Web Component
|
|
2
|
+
|
|
3
|
+
**Category:** Web Components
|
|
4
|
+
**Difficulty:** Medium
|
|
5
|
+
**Suggested Time:** ~35 minutes
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
Build a `<star-rating>` custom element that displays 5 stars, allows users to click to rate, has a `value` attribute/property, and dispatches a `change` event when the rating changes.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- **Display** — Render 5 star elements (you may use Unicode stars ★/☆, SVG, or styled spans).
|
|
16
|
+
- **Click to rate** — Clicking a star sets the rating to that star's index (1–5).
|
|
17
|
+
- **`value` attribute** — The component accepts a `value` attribute (e.g. `<star-rating value="3">`) to show the initial or current rating.
|
|
18
|
+
- **`value` property** — The component exposes a `value` getter/setter that reflects and updates the rating.
|
|
19
|
+
- **`change` event** — When the user clicks a star, dispatch a `change` event with the new value (e.g. `detail: { value: 3 }`).
|
|
20
|
+
|
|
21
|
+
## Example Usage
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<star-rating value="3"></star-rating>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const el = document.querySelector('star-rating');
|
|
29
|
+
el.value = 4;
|
|
30
|
+
el.addEventListener('change', (e) => console.log('New rating:', e.detail.value));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Constraints
|
|
34
|
+
|
|
35
|
+
- Use the Custom Elements API (extend `HTMLElement`).
|
|
36
|
+
- Use Shadow DOM for encapsulation.
|
|
37
|
+
- Observe the `value` attribute and sync it with the internal state.
|
|
38
|
+
- Clamp `value` to 0–5 (0 = no stars selected).
|
|
39
|
+
|
|
40
|
+
## Hints
|
|
41
|
+
|
|
42
|
+
- Use `attachShadow({ mode: 'open' })` in the constructor.
|
|
43
|
+
- Use `static get observedAttributes()` to return `['value']`.
|
|
44
|
+
- In `attributeChangedCallback`, parse the attribute and update the display.
|
|
45
|
+
- Use `CustomEvent` with `detail: { value }` for the change event.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './component';
|
|
3
|
+
|
|
4
|
+
describe('star-rating', () => {
|
|
5
|
+
let el: HTMLElement & { value: number };
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
el = document.createElement('star-rating') as HTMLElement & { value: number };
|
|
9
|
+
document.body.appendChild(el);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
el.remove();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders 5 star elements', () => {
|
|
17
|
+
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
18
|
+
expect(stars.length).toBe(5);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('default value is 0', () => {
|
|
22
|
+
expect(el.value).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('setting value attribute updates display', () => {
|
|
26
|
+
el.setAttribute('value', '3');
|
|
27
|
+
expect(el.value).toBe(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('clicking a star updates value', () => {
|
|
31
|
+
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
32
|
+
(stars[2] as HTMLElement).click();
|
|
33
|
+
expect(el.value).toBe(3);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('dispatches change event on click', () => {
|
|
37
|
+
let receivedValue: number | undefined;
|
|
38
|
+
const handler = (e: Event) => {
|
|
39
|
+
receivedValue = (e as CustomEvent).detail?.value;
|
|
40
|
+
};
|
|
41
|
+
el.addEventListener('change', handler);
|
|
42
|
+
|
|
43
|
+
const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
|
|
44
|
+
(stars[3] as HTMLElement).click();
|
|
45
|
+
|
|
46
|
+
expect(receivedValue).toBe(4);
|
|
47
|
+
el.removeEventListener('change', handler);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('value property reflects attribute', () => {
|
|
51
|
+
el.setAttribute('value', '2');
|
|
52
|
+
expect(el.value).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('setting value property updates attribute', () => {
|
|
56
|
+
el.value = 5;
|
|
57
|
+
expect(el.getAttribute('value')).toBe('5');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('clamps value to 0-5', () => {
|
|
61
|
+
el.setAttribute('value', '10');
|
|
62
|
+
expect(el.value).toBeLessThanOrEqual(5);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class StarRating extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.attachShadow({ mode: 'open' });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
connectedCallback() {
|
|
8
|
+
// TODO: implement - render 5 stars, handle click, support value attribute
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static get observedAttributes() {
|
|
12
|
+
return ['value'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null) {
|
|
16
|
+
// TODO: implement
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get value(): number {
|
|
20
|
+
return 0; // TODO: implement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set value(_val: number) {
|
|
24
|
+
// TODO: implement
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
customElements.define('star-rating', StarRating);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Star Rating Component</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Star Rating</h1>
|
|
10
|
+
<star-rating value="3"></star-rating>
|
|
11
|
+
|
|
12
|
+
<script type="module" src="./component.ts"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|