Skip to content

Commit 8c78405

Browse files
committed
Fix failing Help and PartialList tests, start document how to run and write tests in dev.md
1 parent 9bfb8cc commit 8c78405

File tree

4 files changed

+179
-120
lines changed

4 files changed

+179
-120
lines changed

dev.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
# Development documentation
22

3-
### Architecture
3+
## Testing
4+
5+
The project started without any automated testing because we didn't looked at ink-testing-library and we tested manually. The current state of the project is that is mostly untested automatically and somewhat tested manually. Later, I setup some abstractions and configurations to use ink-testing-library fully, this wasn't easy and not totally done (the store is not reset between e2e tests causing fails).
6+
7+
I started doing 2-3 e2e tests and some component testing for the new `PartialList.tsx` ! But we clearly need to have a lot more e2e tests to cover main feature like watch mode, search, help, browsing exos, exo details display... If you want to contribute some to make it more stable or as a bug reproduction you are more than welcome ! I'm going to write them as I change things or fix bugs.
8+
9+
All tests are present inside `tests/`. We are using Vitest and a few abstractions in `tests/utils/helpers.ts`.
10+
11+
**Running all tests**
12+
```sh
13+
pnpm test
14+
```
15+
**Running all tests and stop**
16+
```sh
17+
pnpm test --run
18+
```
19+
20+
**Running a single test file**
21+
```sh
22+
pnpm test tests/PartialList.test.ts
23+
```
24+
25+
**Tips for writing tests - IMPORTANT**
26+
1. Easily type shortcuts by using `type()`. If you give several letters like `type("jjk")` it will type them one after the other with a short wait timing to simulate a real keyboard and make the shortcuts system consider them indepedant.
27+
1. Before entering any input via `type()`, call once `await waitForInputsToBeReady();` at the start of the test
28+
1. If tests are eating more and more RAM, check if the store might not be closed on component unmount and this would cause to block the exit...
29+
30+
## Architecture
431

532
![architecture diagram](presentation/imgs/architecture.png)
633

734
TODO: document how the project is structured, refactor components and duplications, add unit tests.
835

36+
## Misc
937
### Development bugs
1038

1139
Issues we had during development and their resolutions in case this is useful in the future...

src/Help.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const categorizeShortcuts = (shortcuts: Shortcut[]) => {
3232
return categories;
3333
};
3434

35-
function beautifyPattern(pattern: string) {
35+
export function beautifyPattern(pattern: string) {
3636
return pattern.replace('Arrow', '').replace('ctrl', 'Ctrl');
3737
}
3838

tests/Help.test.tsx

+43-30
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,60 @@
11
// Unit tests on PartialList.tsx
22
import React from 'react';
3-
import { expect, test } from 'vitest'
4-
import { render } from 'ink-testing-library';
3+
import {expect, test} from 'vitest';
4+
import {render} from 'ink-testing-library';
55
import chalk from 'chalk';
6-
import { mustShow, mustContain, mustNotContain, expectNFrames, type } from "./utils/helpers.ts"
7-
import Help, { categorizeShortcuts } from "../src/Help"
8-
import { shortcuts } from '../src/shortcuts.ts';
9-
import { Shortcut } from '../src/types.ts';
6+
import {
7+
mustShow,
8+
mustContain,
9+
mustNotContain,
10+
expectNFrames,
11+
type,
12+
} from './utils/helpers.ts';
13+
import Help, {beautifyPattern, categorizeShortcuts} from '../src/Help';
14+
import {shortcuts} from '../src/shortcuts.ts';
15+
import {Shortcut} from '../src/types.ts';
16+
import {waitForInputsToBeReady} from './utils/shopify-cli-testing-helpers.ts';
1017

1118
test('Help categorizeShortcuts() is correct', () => {
12-
const pack = categorizeShortcuts(shortcuts)
19+
const pack = categorizeShortcuts(shortcuts);
1320
// Contain some common shortcuts
14-
expect(pack.common.length).to.not.eq(0)
15-
expect(pack.common[0].description).to.eq("View help page")
21+
expect(pack.common.length).to.not.eq(0);
22+
expect(pack.common[0].description).to.eq('View help page');
1623

1724
// no non common shortcuts in common section
18-
expect(pack.common.filter(e => e.pattern == 'return').length).to.eq(0)
25+
expect(pack.common.filter(e => e.pattern == 'return').length).to.eq(0);
1926

2027
// Contain some help shortcuts
21-
expect(pack.help[0].description).to.eq("Escape help page")
22-
})
28+
expect(pack.help[0].description).to.eq('Escape help page');
29+
});
2330

2431
test('Help can show all shortcuts', async () => {
2532
const inst = render(<Help height={40} shortcuts={shortcuts} />);
2633
shortcuts.forEach(s => {
27-
mustContain(inst, s.pattern)
28-
mustContain(inst, s.description)
29-
if (s.alt)
30-
mustContain(inst, s.alt)
31-
})
32-
mustContain(inst, "Common shortcuts")
33-
mustContain(inst, "Home shortcuts")
34-
mustContain(inst, "List shortcuts")
35-
mustContain(inst, "Train shortcuts")
36-
mustContain(inst, "Help shortcuts")
37-
inst.unmount()
34+
mustContain(inst, beautifyPattern(s.pattern));
35+
mustContain(inst, s.description);
36+
if (s.alt) mustContain(inst, beautifyPattern(s.alt));
37+
});
38+
mustContain(inst, 'Common shortcuts');
39+
mustContain(inst, 'Home shortcuts');
40+
mustContain(inst, 'List shortcuts');
41+
mustContain(inst, 'Train shortcuts');
42+
mustContain(inst, 'Help shortcuts');
43+
inst.unmount();
3844
});
3945

40-
test('Help use a partial list', async () => {
41-
const firstCommonShortcut: Shortcut = { pattern: 'o', description: 'new shortcut', action: () => { } }
42-
const inst = render(<Help height={10} shortcuts={[firstCommonShortcut, ...shortcuts]} />);
43-
mustContain(inst, "new shortcut")
44-
await type(inst, "jj") //2 j should be just enough to have the "Common shortcut" disappear and the first shortcut too
45-
mustNotContain(inst, "new shortcut")
46-
inst.unmount()
46+
test('Help is using a partial list', async () => {
47+
const firstCommonShortcut: Shortcut = {
48+
pattern: 'o',
49+
description: 'new shortcut',
50+
action: () => {},
51+
};
52+
const inst = render(
53+
<Help height={10} shortcuts={[firstCommonShortcut, ...shortcuts]} />,
54+
);
55+
mustContain(inst, 'new shortcut');
56+
await waitForInputsToBeReady();
57+
await type(inst, 'jj'); //2 j should be just enough to have the "Common shortcut" disappear and the first shortcut too
58+
mustNotContain(inst, 'new shortcut');
59+
inst.unmount();
4760
});

tests/PartialList.test.tsx

+106-88
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,94 @@
11
// Unit tests on PartialList.tsx
22
import React from 'react';
3-
import { describe, expect, test } from 'vitest'
4-
import { Text } from 'ink'
5-
import { render } from 'ink-testing-library';
3+
import {describe, expect, test} from 'vitest';
4+
import {Text} from 'ink';
5+
import {render} from 'ink-testing-library';
66
import chalk from 'chalk';
7-
import { mustShow, mustContain, mustNotContain, expectNFrames, type, typeAndWait } from "./utils/helpers.ts"
8-
import PartialList, { getSliceStartIndex } from "../src/PartialList"
9-
import { BG_VARIANTES } from '../src/util'
10-
import { waitForInputsToBeReady } from './utils/shopify-cli-testing-helpers.ts';
7+
import {mustShow, mustContain, type} from './utils/helpers.ts';
8+
import PartialList, {getSliceStartIndex} from '../src/PartialList';
9+
import {LOGO_COLORS} from '../src/util';
10+
import {waitForInputsToBeReady} from './utils/shopify-cli-testing-helpers.ts';
1111

1212
test('can show a short list of numbers', async () => {
13-
const numbers = [2, 5, 1, 53]
14-
const inst = render(<PartialList
15-
height={50}
16-
list={numbers.map(n => <Text>{n}</Text>)}
17-
selectionEnabled={false}
18-
/>);
19-
20-
mustShow(inst, numbers.join("\n"))
21-
inst.unmount()
13+
const numbers = [2, 5, 1, 53];
14+
const inst = render(
15+
<PartialList
16+
height={50}
17+
list={numbers.map(n => (
18+
<Text>{n}</Text>
19+
))}
20+
selectionEnabled={false}
21+
/>,
22+
);
23+
24+
mustShow(inst, numbers.join('\n'));
25+
inst.unmount();
2226
});
2327

24-
const NUMBERS = [2, 5, 1, 53, 2, 4, 6, 1, 4, 2, 1, 3, 5, 6, 2, 1, 5, 6, 2, 10, 6, 2, 5, 22]
25-
const HEIGHT = 10
28+
const NUMBERS = [
29+
2, 5, 1, 53, 2, 4, 6, 1, 4, 2, 1, 3, 5, 6, 2, 1, 5, 6, 2, 10, 6, 2, 5, 22,
30+
];
31+
const HEIGHT = 10;
2632

2733
test('do not print the end of the list when very long', async () => {
28-
const inst = render(<PartialList
29-
height={HEIGHT}
30-
list={NUMBERS.map(n => <Text>{n}</Text>)}
31-
selectionEnabled={false}
32-
/>);
33-
34-
mustShow(inst, NUMBERS.slice(0, HEIGHT).join("\n"))
35-
inst.unmount()
34+
const inst = render(
35+
<PartialList
36+
height={HEIGHT}
37+
list={NUMBERS.map(n => (
38+
<Text>{n}</Text>
39+
))}
40+
selectionEnabled={false}
41+
/>,
42+
);
43+
44+
mustShow(inst, NUMBERS.slice(0, HEIGHT).join('\n'));
45+
inst.unmount();
3646
});
3747

3848
test('show selected item when selection is enabled', async () => {
3949
// Selection enabled
40-
const inst = render(<PartialList
41-
height={HEIGHT}
42-
list={NUMBERS.map(n => <Text>{n}</Text>)}
43-
selectionEnabled={true}
44-
selectionIndex={3} //4rd element
45-
/>);
46-
47-
mustContain(inst, NUMBERS.slice(0, 3).join("\n"))
48-
mustContain(inst, chalk.bgHex(BG_VARIANTES[0]).black(NUMBERS[3]))
49-
mustContain(inst, NUMBERS.slice(5, HEIGHT).join("\n"))
50+
const inst = render(
51+
<PartialList
52+
height={HEIGHT}
53+
list={NUMBERS.map(n => (
54+
<Text>{n}</Text>
55+
))}
56+
selectionEnabled={true}
57+
selectionIndex={3} //4rd element
58+
/>,
59+
);
60+
61+
mustContain(inst, NUMBERS.slice(0, 3).join('\n'));
62+
mustContain(inst, chalk.bgHex(LOGO_COLORS[0]).black(NUMBERS[3]));
63+
mustContain(inst, NUMBERS.slice(5, HEIGHT).join('\n'));
5064
// Selection disabled
51-
inst.unmount()
65+
inst.unmount();
5266
});
5367

5468
test('getSliceStartIndex() is correct', () => {
5569
// When selection mode is disabled, start index should be equal to selectedIndex
5670
// and should not exceed max
57-
expect(getSliceStartIndex(false, 0, 10, 100)).to.eq(0)
58-
expect(getSliceStartIndex(false, 2, 10, 100)).to.eq(2)
59-
expect(getSliceStartIndex(false, 90, 5, 100)).to.eq(90)
60-
expect(getSliceStartIndex(false, 95, 5, 100)).to.eq(95)
61-
expect(getSliceStartIndex(false, 96, 5, 100)).to.eq(95)
62-
expect(getSliceStartIndex(false, 99, 5, 100)).to.eq(95)
71+
expect(getSliceStartIndex(false, 0, 10, 100)).to.eq(0);
72+
expect(getSliceStartIndex(false, 2, 10, 100)).to.eq(2);
73+
expect(getSliceStartIndex(false, 90, 5, 100)).to.eq(90);
74+
expect(getSliceStartIndex(false, 95, 5, 100)).to.eq(95);
75+
expect(getSliceStartIndex(false, 96, 5, 100)).to.eq(95);
76+
expect(getSliceStartIndex(false, 99, 5, 100)).to.eq(95);
6377

6478
// When selection mode is enabled, start index = 0 if i < h, else i - h + 1 and should not exceed max
65-
expect(getSliceStartIndex(true, 0, 5, 100)).to.eq(0)
66-
expect(getSliceStartIndex(true, 3, 5, 100)).to.eq(0)
67-
expect(getSliceStartIndex(true, 4, 5, 100)).to.eq(0)
68-
expect(getSliceStartIndex(true, 5, 5, 100)).to.eq(1)
69-
expect(getSliceStartIndex(true, 10, 5, 100)).to.eq(6)
70-
expect(getSliceStartIndex(true, 95, 5, 100)).to.eq(91)
71-
expect(getSliceStartIndex(true, 96, 5, 100)).to.eq(92)
72-
expect(getSliceStartIndex(true, 99, 5, 100)).to.eq(95)
79+
expect(getSliceStartIndex(true, 0, 5, 100)).to.eq(0);
80+
expect(getSliceStartIndex(true, 3, 5, 100)).to.eq(0);
81+
expect(getSliceStartIndex(true, 4, 5, 100)).to.eq(0);
82+
expect(getSliceStartIndex(true, 5, 5, 100)).to.eq(1);
83+
expect(getSliceStartIndex(true, 10, 5, 100)).to.eq(6);
84+
expect(getSliceStartIndex(true, 95, 5, 100)).to.eq(91);
85+
expect(getSliceStartIndex(true, 96, 5, 100)).to.eq(92);
86+
expect(getSliceStartIndex(true, 99, 5, 100)).to.eq(95);
7387

7488
// When list length < height, test that is doesn't became negative...
75-
expect(getSliceStartIndex(false, 3, 10, 5)).to.eq(0)
76-
expect(getSliceStartIndex(true, 4, 10, 5)).to.eq(0)
77-
})
89+
expect(getSliceStartIndex(false, 3, 10, 5)).to.eq(0);
90+
expect(getSliceStartIndex(true, 4, 10, 5)).to.eq(0);
91+
});
7892

7993
// test('shift the view via selectionIndex when selection is enabled', async () => {
8094
// const inst = render(<PartialList
@@ -89,43 +103,47 @@ test('getSliceStartIndex() is correct', () => {
89103
// });
90104

91105
test('show a message when the list is empty', async () => {
92-
const inst = render(<PartialList
93-
height={HEIGHT}
94-
list={[]}
95-
selectionEnabled={false}
96-
/>);
97-
mustShow(inst, "No element to show.")
98-
inst.unmount()
99-
})
106+
const inst = render(
107+
<PartialList height={HEIGHT} list={[]} selectionEnabled={false} />,
108+
);
109+
mustShow(inst, 'No element to show.');
110+
inst.unmount();
111+
});
100112

101113
test('support a custom empty list message', async () => {
102-
const inst = render(<PartialList
103-
height={HEIGHT}
104-
list={[]}
105-
selectionEnabled={false}
106-
emptyListMessage='error: custom message'
107-
/>);
108-
mustShow(inst, "\u001b[31mcustom message\u001b[39m") //in red
109-
inst.unmount()
114+
const inst = render(
115+
<PartialList
116+
height={HEIGHT}
117+
list={[]}
118+
selectionEnabled={false}
119+
emptyListMessage="error: custom message"
120+
/>,
121+
);
122+
mustShow(inst, '\u001b[31mcustom message\u001b[39m'); //in red
123+
inst.unmount();
110124
});
111125

112126
test('manage scroll progress without selectionIndex when selectionEnabled=false', async () => {
113-
const inst = render(<PartialList
114-
height={HEIGHT}
115-
list={NUMBERS.map(n => <Text>{n}</Text>)}
116-
selectionEnabled={false}
117-
emptyListMessage='error: custom message'
118-
/>);
119-
await waitForInputsToBeReady()
120-
mustShow(inst, NUMBERS.slice(0, HEIGHT).join("\n"))
121-
122-
await type(inst, "j")
123-
mustShow(inst, NUMBERS.slice(1, HEIGHT + 1).join("\n"))
124-
125-
await type(inst, "k")
126-
mustShow(inst, NUMBERS.slice(0, HEIGHT).join("\n"))
127-
128-
await type(inst, "jjjjjkk")
129-
mustShow(inst, NUMBERS.slice(3, HEIGHT + 3).join("\n"))
130-
inst.unmount()
127+
const inst = render(
128+
<PartialList
129+
height={HEIGHT}
130+
list={NUMBERS.map(n => (
131+
<Text>{n}</Text>
132+
))}
133+
selectionEnabled={false}
134+
emptyListMessage="error: custom message"
135+
/>,
136+
);
137+
await waitForInputsToBeReady();
138+
mustShow(inst, NUMBERS.slice(0, HEIGHT).join('\n'));
139+
140+
await type(inst, 'j');
141+
mustShow(inst, NUMBERS.slice(1, HEIGHT + 1).join('\n'));
142+
143+
await type(inst, 'k');
144+
mustShow(inst, NUMBERS.slice(0, HEIGHT).join('\n'));
145+
146+
await type(inst, 'jjjjjkk');
147+
mustShow(inst, NUMBERS.slice(3, HEIGHT + 3).join('\n'));
148+
inst.unmount();
131149
});

0 commit comments

Comments
 (0)