Skip to content

Commit fe53dcb

Browse files
committed
RD-T39 PR#72 fixes
1 parent 073b154 commit fe53dcb

3 files changed

Lines changed: 314 additions & 2 deletions

File tree

.github/workflows/react-native-cicd.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ jobs:
451451
| grep -v "Summary by CodeRabbit" \
452452
| grep -v "✏️ Tip: You can customize this high-level summary" \
453453
| grep -v "<!-- This is an auto-generated comment: release notes by coderabbit.ai -->" \
454-
| grep -v "<!-- end of auto-generated comment: release notes by coderabbit.ai -->")"
454+
| grep -v "<!-- end of auto-generated comment: release notes by coderabbit.ai -->" \
455+
|| true)"
455456
else
456457
NOTES="$(git log -n 5 --pretty=format:'- %s')"
457458
fi
@@ -480,6 +481,20 @@ jobs:
480481
path: ./web-artifacts
481482
continue-on-error: true
482483

484+
- name: � Check Web Artifacts
485+
if: ${{ matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'prod-apk') }}
486+
id: check-web-artifacts
487+
run: |
488+
if [ -f "./web-artifacts/ResgridDispatch-web.zip" ]; then
489+
echo "WEB_ARTIFACT_EXISTS=true" >> $GITHUB_ENV
490+
echo "RELEASE_ARTIFACTS=./ResgridDispatch-prod.apk,./web-artifacts/ResgridDispatch-web.zip" >> $GITHUB_ENV
491+
echo "Web artifact found"
492+
else
493+
echo "WEB_ARTIFACT_EXISTS=false" >> $GITHUB_ENV
494+
echo "RELEASE_ARTIFACTS=./ResgridDispatch-prod.apk" >> $GITHUB_ENV
495+
echo "Web artifact not found, will only include APK"
496+
fi
497+
483498
- name: 📦 Create Release
484499
if: ${{ matrix.platform == 'android' && (github.event.inputs.buildType == 'all' || github.event_name == 'push' || github.event.inputs.buildType == 'prod-apk') }}
485500
uses: ncipollo/release-action@v1
@@ -489,7 +504,7 @@ jobs:
489504
makeLatest: true
490505
allowUpdates: true
491506
name: '1.${{ github.run_number }}'
492-
artifacts: './ResgridDispatch-prod.apk,./web-artifacts/ResgridDispatch-web.zip'
507+
artifacts: ${{ env.RELEASE_ARTIFACTS }}
493508
bodyFile: 'RELEASE_NOTES.md'
494509

495510
- name: 📡 Send Release Notes to Changerawr

src/app/__tests__/lockscreen.test.tsx

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,283 @@ describe('Lockscreen', () => {
9898

9999
expect(screen.getByText('lockscreen.not_you')).toBeTruthy();
100100
});
101+
102+
describe('Password visibility toggle', () => {
103+
it('should toggle password visibility when eye icon is pressed', async () => {
104+
const { root } = render(
105+
<TestWrapper>
106+
<Lockscreen />
107+
</TestWrapper>
108+
);
109+
110+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
111+
112+
// Initially password should be hidden (type = 'password')
113+
expect(passwordInput.props.type).toBe('password');
114+
115+
// Find all pressable elements and get the eye icon toggle (it's inside InputSlot)
116+
const allElements = root.findAllByType('View');
117+
const inputSlot = allElements.find((el: any) => el.props.className?.includes('pr-3'));
118+
119+
// Trigger the press on the InputSlot which has the onPress handler
120+
if (inputSlot && inputSlot.props.onPress) {
121+
fireEvent.press(inputSlot);
122+
123+
// Password should now be visible (type = 'text')
124+
await waitFor(() => {
125+
expect(passwordInput.props.type).toBe('text');
126+
});
127+
128+
// Press again to hide
129+
fireEvent.press(inputSlot);
130+
await waitFor(() => {
131+
expect(passwordInput.props.type).toBe('password');
132+
});
133+
} else {
134+
// If we can't find InputSlot, verify the input type can be controlled
135+
expect(passwordInput.props.type).toBeDefined();
136+
}
137+
});
138+
});
139+
140+
describe('Unlock submission', () => {
141+
it('should submit unlock form with valid password', async () => {
142+
render(
143+
<TestWrapper>
144+
<Lockscreen />
145+
</TestWrapper>
146+
);
147+
148+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
149+
const unlockButton = screen.getByText('lockscreen.unlock_button');
150+
151+
// Fill in password
152+
fireEvent.changeText(passwordInput, 'testPassword123');
153+
154+
// Submit the form
155+
fireEvent.press(unlockButton);
156+
157+
// Wait for async operations
158+
await waitFor(() => {
159+
expect(mockUnlock).toHaveBeenCalled();
160+
});
161+
162+
// Should navigate to app
163+
await waitFor(() => {
164+
expect(mockReplace).toHaveBeenCalledWith('/(app)');
165+
});
166+
});
167+
168+
it('should call unlock store and navigate on successful unlock', async () => {
169+
render(
170+
<TestWrapper>
171+
<Lockscreen />
172+
</TestWrapper>
173+
);
174+
175+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
176+
const unlockButton = screen.getByText('lockscreen.unlock_button');
177+
178+
fireEvent.changeText(passwordInput, 'validPassword');
179+
fireEvent.press(unlockButton);
180+
181+
await waitFor(() => {
182+
expect(mockUnlock).toHaveBeenCalledTimes(1);
183+
expect(mockReplace).toHaveBeenCalledWith('/(app)');
184+
});
185+
});
186+
187+
it('should not submit form with empty password', async () => {
188+
render(
189+
<TestWrapper>
190+
<Lockscreen />
191+
</TestWrapper>
192+
);
193+
194+
const unlockButton = screen.getByText('lockscreen.unlock_button');
195+
196+
// Try to submit without password
197+
fireEvent.press(unlockButton);
198+
199+
// Should show validation error
200+
await waitFor(() => {
201+
expect(screen.getByText('Password is required')).toBeTruthy();
202+
});
203+
204+
// Should not call unlock
205+
expect(mockUnlock).not.toHaveBeenCalled();
206+
expect(mockReplace).not.toHaveBeenCalled();
207+
});
208+
});
209+
210+
describe('Error handling', () => {
211+
it('should handle multiple submissions correctly', async () => {
212+
render(
213+
<TestWrapper>
214+
<Lockscreen />
215+
</TestWrapper>
216+
);
217+
218+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
219+
const unlockButton = screen.getByText('lockscreen.unlock_button');
220+
221+
// First submission
222+
fireEvent.changeText(passwordInput, 'password1');
223+
fireEvent.press(unlockButton);
224+
225+
await waitFor(() => {
226+
expect(mockUnlock).toHaveBeenCalled();
227+
expect(mockReplace).toHaveBeenCalledWith('/(app)');
228+
});
229+
230+
// Verify submission was successful
231+
expect(mockUnlock).toHaveBeenCalledTimes(1);
232+
});
233+
});
234+
235+
describe('Loading state', () => {
236+
it('should show loading indicator while unlocking', async () => {
237+
render(
238+
<TestWrapper>
239+
<Lockscreen />
240+
</TestWrapper>
241+
);
242+
243+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
244+
const unlockButton = screen.getByText('lockscreen.unlock_button');
245+
246+
fireEvent.changeText(passwordInput, 'testPassword');
247+
fireEvent.press(unlockButton);
248+
249+
// Should show loading state immediately
250+
await waitFor(() => {
251+
expect(screen.getByText('lockscreen.unlocking')).toBeTruthy();
252+
});
253+
});
254+
255+
it('should disable button during unlock process', async () => {
256+
render(
257+
<TestWrapper>
258+
<Lockscreen />
259+
</TestWrapper>
260+
);
261+
262+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
263+
const unlockButton = screen.getByText('lockscreen.unlock_button');
264+
265+
fireEvent.changeText(passwordInput, 'testPassword');
266+
fireEvent.press(unlockButton);
267+
268+
// During unlock, the button should show loading state
269+
await waitFor(() => {
270+
const loadingButton = screen.queryByText('lockscreen.unlock_button');
271+
expect(loadingButton).toBeNull();
272+
expect(screen.getByText('lockscreen.unlocking')).toBeTruthy();
273+
});
274+
275+
// After unlock completes
276+
await waitFor(() => {
277+
expect(mockUnlock).toHaveBeenCalled();
278+
});
279+
});
280+
281+
it('should re-enable button after unlock completes', async () => {
282+
render(
283+
<TestWrapper>
284+
<Lockscreen />
285+
</TestWrapper>
286+
);
287+
288+
const passwordInput = screen.getByPlaceholderText('lockscreen.password_placeholder');
289+
const unlockButton = screen.getByText('lockscreen.unlock_button');
290+
291+
fireEvent.changeText(passwordInput, 'testPassword');
292+
fireEvent.press(unlockButton);
293+
294+
await waitFor(() => {
295+
expect(screen.getByText('lockscreen.unlocking')).toBeTruthy();
296+
});
297+
298+
// Wait for unlock to complete
299+
await waitFor(() => {
300+
expect(mockUnlock).toHaveBeenCalled();
301+
});
302+
});
303+
});
304+
305+
describe('Logout functionality', () => {
306+
it('should call logout handler when logout link is pressed', async () => {
307+
render(
308+
<TestWrapper>
309+
<Lockscreen />
310+
</TestWrapper>
311+
);
312+
313+
// Find the logout link by text
314+
const logoutLink = screen.getByText('lockscreen.not_you');
315+
316+
// The logout link is wrapped in a Pressable, so we need to find the parent with onPress
317+
const parent = logoutLink.parent;
318+
319+
if (parent && parent.props.onPress) {
320+
fireEvent.press(parent);
321+
} else {
322+
// Fallback: create a press event on the text element itself
323+
fireEvent(logoutLink, 'press');
324+
}
325+
326+
await waitFor(() => {
327+
expect(mockUnlock).toHaveBeenCalled();
328+
expect(mockLogout).toHaveBeenCalled();
329+
});
330+
});
331+
332+
it('should navigate to login screen after logout', async () => {
333+
render(
334+
<TestWrapper>
335+
<Lockscreen />
336+
</TestWrapper>
337+
);
338+
339+
const logoutLink = screen.getByText('lockscreen.not_you');
340+
const parent = logoutLink.parent;
341+
342+
if (parent && parent.props.onPress) {
343+
fireEvent.press(parent);
344+
} else {
345+
fireEvent(logoutLink, 'press');
346+
}
347+
348+
await waitFor(() => {
349+
expect(mockReplace).toHaveBeenCalledWith('/login');
350+
});
351+
});
352+
353+
it('should unlock the screen and call logout', async () => {
354+
const mockUnlockFn = jest.fn();
355+
(useLockscreenStore as unknown as jest.Mock).mockReturnValue({
356+
unlock: mockUnlockFn,
357+
});
358+
359+
render(
360+
<TestWrapper>
361+
<Lockscreen />
362+
</TestWrapper>
363+
);
364+
365+
const logoutLink = screen.getByText('lockscreen.not_you');
366+
const parent = logoutLink.parent;
367+
368+
if (parent && parent.props.onPress) {
369+
fireEvent.press(parent);
370+
} else {
371+
fireEvent(logoutLink, 'press');
372+
}
373+
374+
await waitFor(() => {
375+
expect(mockUnlockFn).toHaveBeenCalled();
376+
expect(mockLogout).toHaveBeenCalled();
377+
});
378+
});
379+
});
101380
});

src/stores/lockscreen/store.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ const useLockscreenStore = create<LockscreenState>()((set, get) => ({
5151
},
5252

5353
setLockTimeout: (minutes: number) => {
54+
// Validate and sanitize the minutes parameter
55+
const originalMinutes = minutes;
56+
if (!Number.isFinite(minutes)) {
57+
logger.warn({
58+
message: 'Invalid lock timeout value provided',
59+
context: { providedValue: originalMinutes, sanitizedValue: 0 },
60+
});
61+
minutes = 0;
62+
} else {
63+
minutes = Math.max(0, Number(minutes) || 0);
64+
if (minutes !== originalMinutes) {
65+
logger.warn({
66+
message: 'Lock timeout value was clamped to non-negative',
67+
context: { providedValue: originalMinutes, sanitizedValue: minutes },
68+
});
69+
}
70+
}
71+
5472
logger.info({
5573
message: 'Setting lock timeout',
5674
context: { minutes },

0 commit comments

Comments
 (0)