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
8 changes: 6 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ android {
applicationId 'com.pgarr.simplenotepad'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 23
versionName "1.6.0"
versionCode 24
versionName "1.6.1"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down Expand Up @@ -179,4 +179,8 @@ dependencies {
} else {
implementation jscFlavor
}

testImplementation 'junit:junit:4.13.2'
// Android stubs org.json with no-op implementations; this provides the real one for JVM tests.
testImplementation 'org.json:json:20231013'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.pgarr.simplenotepad.widget

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class WidgetDbHelperTest {

@Test
fun `parseItems returns empty list for invalid JSON`() {
val result = WidgetDbHelper.parseItems("not json at all")
assertTrue(result.isEmpty())
}

@Test
fun `parseItems returns empty list for empty JSON array`() {
val result = WidgetDbHelper.parseItems("[]")
assertTrue(result.isEmpty())
}

@Test
fun `parseItems returns empty list for JSON object instead of array`() {
val result = WidgetDbHelper.parseItems("""{"checked":false,"text":"x"}""")
assertTrue(result.isEmpty())
}

@Test
fun `parseItems parses a single valid item`() {
val result = WidgetDbHelper.parseItems("""[{"checked":false,"text":"buy milk"}]""")
assertEquals(1, result.size)
assertEquals("buy milk", result[0].text)
assertFalse(result[0].checked)
}

@Test
fun `parseItems parses checked true correctly`() {
val result = WidgetDbHelper.parseItems("""[{"checked":true,"text":"done"}]""")
assertTrue(result[0].checked)
}

@Test
fun `parseItems defaults checked to false when field is missing`() {
val result = WidgetDbHelper.parseItems("""[{"text":"no checked field"}]""")
assertEquals(1, result.size)
assertFalse(result[0].checked)
}

@Test
fun `parseItems defaults text to empty string when field is missing`() {
val result = WidgetDbHelper.parseItems("""[{"checked":false}]""")
assertEquals(1, result.size)
assertEquals("", result[0].text)
}

@Test
fun `parseItems sets originalIndex matching position in source array`() {
val json = """[
{"checked":false,"text":"a"},
{"checked":true,"text":"b"},
{"checked":false,"text":"c"}
]"""
val result = WidgetDbHelper.parseItems(json)
assertEquals(3, result.size)
assertEquals(0, result[0].originalIndex)
assertEquals(1, result[1].originalIndex)
assertEquals(2, result[2].originalIndex)
}

@Test
fun `parseItems skips null entries in the array`() {
// JSON array with a null entry between two valid objects
val json = """[{"checked":false,"text":"first"},null,{"checked":true,"text":"third"}]"""
val result = WidgetDbHelper.parseItems(json)
assertEquals(2, result.size)
assertEquals("first", result[0].text)
assertEquals("third", result[1].text)
// originalIndex reflects position in original array, skipping nulls
assertEquals(0, result[0].originalIndex)
assertEquals(2, result[1].originalIndex)
}

@Test
fun `parseItems round-trips multiple items preserving all fields`() {
val json = """[
{"checked":false,"text":"walk the dog"},
{"checked":true,"text":"buy groceries"}
]"""
val result = WidgetDbHelper.parseItems(json)
assertEquals(2, result.size)
assertEquals(WidgetListItem(text = "walk the dog", checked = false, originalIndex = 0), result[0])
assertEquals(WidgetListItem(text = "buy groceries", checked = true, originalIndex = 1), result[1])
}
}
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "simple-notepad",
"slug": "simple-notepad",
"version": "1.6.0",
"version": "1.6.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "simple-notepad",
Expand All @@ -24,7 +24,7 @@
"backgroundColor": "#ffffff"
},
"package": "com.pgarr.simplenotepad",
"versionCode": 23
"versionCode": 24
},
"web": {
"bundler": "metro",
Expand All @@ -43,7 +43,7 @@
"projectId": "9e3820b7-558b-4bd2-a1b2-e49561e741e6"
}
},
"runtimeVersion": "1.6.0",
"runtimeVersion": "1.6.1",
"updates": {
"url": "https://u.expo.dev/9e3820b7-558b-4bd2-a1b2-e49561e741e6"
}
Expand Down
92 changes: 92 additions & 0 deletions components/__tests__/ListForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { act, fireEvent, render } from '@testing-library/react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { ListForm } from '@/components/ListForm';

jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn(),
}));

// `components/ui/text` imports `@rn-primitives/slot` which ships JSX in its
// distributed JS — Jest fails to parse it without this mock.
jest.mock('@rn-primitives/slot', () => ({
Text: () => null,
}));

jest.mock('lucide-react-native', () => ({
Trash2Icon: () => null,
}));

const mockedUseSafeAreaInsets = useSafeAreaInsets as unknown as jest.Mock;

describe('components/ListForm', () => {
beforeEach(() => {
mockedUseSafeAreaInsets.mockReturnValue({ top: 0, right: 0, bottom: 0, left: 0 });
});

it('calls onSave with trimmed title and current items', async () => {
const onSave = jest.fn().mockResolvedValue(undefined);

const { getByPlaceholderText, getByText } = render(
<ListForm
initialItems={[{ checked: false, text: 'buy milk' }]}
onSave={onSave}
submitLabel="Save"
/>,
);

fireEvent.changeText(getByPlaceholderText('Title'), ' My List ');

await act(async () => {
fireEvent.press(getByText('Save'));
});

expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith('My List', [{ checked: false, text: 'buy milk' }]);
});

it('does not call onSave when title is empty or whitespace', () => {
const onSave = jest.fn().mockResolvedValue(undefined);

const { getByText } = render(<ListForm onSave={onSave} submitLabel="Save" />);

fireEvent.press(getByText('Save'));

expect(onSave).not.toHaveBeenCalled();
});

it('pressing Add appends a new empty item row', () => {
const onSave = jest.fn();

const { getByText, queryByPlaceholderText, getByPlaceholderText } = render(
<ListForm onSave={onSave} />,
);

expect(queryByPlaceholderText('Item 1')).toBeNull();

fireEvent.press(getByText('Add'));

expect(getByPlaceholderText('Item 1')).toBeTruthy();
});

it('deleting an item removes it and preserves the remaining item', () => {
const onSave = jest.fn();

const { getByLabelText, queryByPlaceholderText, getByDisplayValue } = render(
<ListForm
initialItems={[
{ checked: false, text: 'first' },
{ checked: false, text: 'second' },
]}
onSave={onSave}
/>,
);

expect(queryByPlaceholderText('Item 2')).toBeTruthy();

fireEvent.press(getByLabelText('Delete item 1'));

expect(queryByPlaceholderText('Item 2')).toBeNull();
expect(getByDisplayValue('second')).toBeTruthy();
});
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "simple-notepad",
"main": "expo-router/entry",
"version": "1.6.0",
"version": "1.6.1",
"scripts": {
"prebuild": "expo prebuild",
"dev": "expo start",
Expand Down
Loading