It is our choices, Harry, that show what we truly are, far more than our abilities, J.K. Rowling, Harry Potter and the Chamber of Secrets

This post teaches you how to save, read, list, and delete user‑created files inside your app. That’s the core of any note‑taking, journaling, or diary feature. The files will be plain Markdown (.md) so that they are human‑readable and easy to edit anywhere.
We’ll use expo-file-system, a library that gives us functions to work with the device’s file system in a cross‑platform way (Android & iOS). You’ll learn:
BUNDLED ASSETS (data/) DOCUMENT DIRECTORY (FileSystem.documentDirectory)
──────────────────── ─────────────────────────────── ───────────────────────────────────────────────────
Where they come from Shipped with the app inside the .apk/.ipa Created after the app is installed, by your code or the user
Example paths data/daily_plan.txt, data/prayer_videos.txt FileSystem.documentDirectory + "diary/2026-07-27.md"
Can you change them? No – they are part of the app’s install package Yes – you can create, edit, delete them whenever you want
How you access them require() + Asset.fromModule() FileSystem.readAsStringAsync(), FileSystem.documentDirectory + "diary/"
What they are for App data that never changes (prayer texts, config files) User data (diary entries, settings, drafts)
The document directory is sandboxed: only your app can see or touch those files. No other app can access them, and the user cannot accidentally open them with a file manager (unless you choose to share them).
On a real device the full path looks something like:
Android: file:///data/user/0/com.yourname.prayercompanion/files/
iOS: file:///var/mobile/Containers/Data/Application/<uuid>/Documents/
expo-file-system gives you a handy constant, FileSystem.documentDirectory, that always points to this location. You just append your own folder names like “diary/”.
Think of a folder (directory) like a box. You can’t put a file into a box that doesn’t exist yet. In many programming languages, if you try to write a file into a folder that hasn’t been created, the write will fail.
With expo-file-system you need to check and create the folder explicitly:
import * as FileSystem from 'expo-file-system';
const diaryDir = FileSystem.documentDirectory + 'diary/';
const dirInfo = await FileSystem.getInfoAsync(diaryDir);
if (!dirInfo.exists) {
await FileSystem.makeDirectoryAsync(diaryDir, { intermediates: true });
}
getInfoAsync(path) returns an object { exists: boolean, isDirectory: boolean, … }.
makeDirectoryAsync(path, options) creates the folder.
{ intermediates: true } means “create any parent folders that are also missing” (like mkdir -p in the terminal).
We’ll wrap this in a helper function ensureDiaryDir() so that every other function can just call it first.
src/services/diaryService.tsThis file is a “service” – a reusable piece of code that handles all the reading and writing of diary entries. What the diary service does?
// src/services/diaryService.ts
//
// DIARY SERVICE
// ──────────────
// Reads, writes, lists, and deletes diary entries.
// Each entry is a single Markdown file named "YYYY-MM-DD.md" inside
// the app's document directory, under a "diary/" subfolder.
//
// STORAGE LOCATION:
// /diary/2026-05-14.md
// The “document directory” is a safe, private folder that belongs only to your app. No other app can see it.
// The diary/ subfolder keeps all the entries together so they don’t get mixed with other files your app might create.
// Because the file name is just the date, you can immediately tell which day a file belongs to without opening it.
//
//
// FILE FORMAT:
// Plain Markdown text. No frontmatter, no special structure required.
// Example content:
// # Morning Reflection
//
// Today I am grateful for...
//
//
// NAMING CONVENTION:
// Files are named YYYY-MM-DD.md (the ISO date format).
// This has two big benefits:
// 1. **Alphabetical order = chronological order**
// 2026-05-01.md comes before 2026-05-10.md automatically when a computer sorts the names,
// so listing entries from oldest to newest is effortless.
// 2. **The date is in the name**
// To know which entry a file holds, you just look at the filename. You never need to open a file to read a date stamp.
//
// Error handling philosophy (the “no stress” rules).
// The service is designed to be gentle, so your app never crashes on a simple missing file:
// - Reading a non‑existent entry, i.e., the file does not exist, (readEntry()) returns "", an empty string.
// This lets your editor show a blank canvas, ready for a new diary entry, without any error.
// - Writing an entry writeEntry() automatically creates the diary/ folder if it doesn’t exist yet.
// - Deleting an entry that doesn’t exist deleteEntry() is a no-op, does nothing and doesn’t throw an error.
// - Listing entries when the folder doesn’t exist yet (e.g. first ever launch) listEntries()
// returns an empty list [] instead of crashing.
// IMPORTS
// expo-file-system/legacy is a library that lets the app read and write files on the device.
// It gives tools like getInfoAsync (check if a file exists), readAsStringAsync (read text),
// and makeDirectoryAsync (create folders).
import * as FileSystem from "expo-file-system/legacy";
// The type imports are just descriptions of data shapes (TypeScript types).
// ISODate is a string like "2026-05-14".
import type { DiaryEntry, DiaryEntrySummary, ISODate } from "../db/types";
// ══════════════════════════════════════════════════════════════════════════════
// PATHS
// ══════════════════════════════════════════════════════════════════════════════
/**
* The diary directory path, inside the app's document directory.
* This puts together the full path to the diary/ folder.
* The trailing / is required by the file‑system library to treat it as a folder.
* It never changes, so it’s a constant.
*/
const DIARY_DIR = `${FileSystem.documentDirectory}diary/`;
/**
* Build the full file path for a given date.
*
* @param date Takes an ISO date string "YYYY-MM-DD", e.g., "2026-05-14"
* @returns Glues together the diary directory, the date, and .md to make a full file path
*
* @example
* entryPath("2026-05-14")
* // → "file:///data/user/0/.../files/diary/2026-05-14.md"
*/
function entryPath(date: ISODate): string {
return `${DIARY_DIR}${date}.md`;
}
/**
* Validate/Checks if the string looks exactly like YYYY-MM-DD (four digits, dash, two digits, dash, two digits)
* 1. It rejects malformed strings before they ever become filenames.
* 2. It prevents a security problem called “path traversal” – for example,
* if someone tried to use ../../etc/passwd as a date, it would be rejected because it doesn’t match the pattern.
*/
function isValidISODate(date: string): boolean {
return /^\d{4}-\d{2}-\d{2}$/.test(date);
}
// ══════════════════════════════════════════════════════════════════════════════
// DIRECTORY SETUP
// ══════════════════════════════════════════════════════════════════════════════
/**
* Ensure the diary directory exists, creating it if necessary.
* This is called automatically by other functions like writeEntry() and listEntries().
* You’ll never need to call it yourself.
*/
async function ensureDiaryDir(): Promise<void> {
// Checks whether the diary/ folder already exists.
const info = await FileSystem.getInfoAsync(DIARY_DIR);
// If not, it creates it, including any missing parent folders
// (intermediates: true acts like mkdir -p in a terminal).
if (!info.exists) {
await FileSystem.makeDirectoryAsync(DIARY_DIR, { intermediates: true });
// It logs a message so you can see when the folder is first created.
console.log(`[Diary] Created directory: ${DIARY_DIR}`);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// READ
// ══════════════════════════════════════════════════════════════════════════════
/**
* Read the content of a diary entry.
* Because readEntry is exported with export async function, other parts of your app can import and use it.
*
* @param date ISO date string "YYYY-MM-DD"
* @returns The Markdown content, or "" if the entry does not exist.
* NEVER throws for a missing file — "" means "new entry".
*
* @example
* const content = await readEntry("2026-05-14");
* if (content === "") {
* console.log("No entry yet for this date.");
* }
*/
export async function readEntry(date: ISODate): Promise<string> {
// Step 1 – Validate the date:
// If the date string doesn’t match the YYYY-MM-DD pattern,
// it logs a warning and immediately returns an empty string. No crash.
if (!isValidISODate(date)) {
console.warn(`[Diary] readEntry: invalid date "${date}"`);
return "";
}
// Step 2 – Build the path and check if the file exists:
const path = entryPath(date);
// It asks the file system for information about the file.
const info = await FileSystem.getInfoAsync(path);
if (!info.exists) {
// If the file does not exist, it returns "".
// This is the key design decision: no error, just an empty string,
// which means “no entry yet for that day”.
return "";
}
// Step 3 – Read the file:
try {
// If the file exists, it reads the whole text content and returns it.
const content = await FileSystem.readAsStringAsync(path);
return content;
} catch (e) {
// If something goes wrong during the actual reading (a very rare disk error), it logs the error and also returns "".
// The app stays stable.
console.error(`[Diary] Failed to read ${path}:`, e);
return "";
}
}
// ══════════════════════════════════════════════════════════════════════════════
// WRITE
// ══════════════════════════════════════════════════════════════════════════════
/**
* Creates or overwrites the diary file for a given date with the full Markdown text you give it.
*
* Creates the diary directory if it does not exist yet (e.g. first entry
* ever written).
*
* WHY "overwrite, not append":
* The diary editor holds the FULL text of the entry in memory and saves the entire content each time —
* there is no concept of "appending a line" in a Markdown editor. Every save replaces the whole file.
* This makes the operation simpler and avoids accidentally duplicating content.
*
* @param date ISO date string "YYYY-MM-DD" (like "2026-05-14").
* @param content The full Markdown text to save
* @throws Error if the date is invalid or the write fails
*
* @example
* await writeEntry("2026-05-14", "# Morning Reflection\n\nGrateful for...");
* …it will create or replace the file diary/2026-05-14.md with that exact text.
*/
// export – makes this function available to other parts of the app
// async function – tells the computer “this function will do things that take time
// (like file operations) and we’ll wait for them to finish before moving on.”
// : Promise – the function returns a Promise that, when completed,
// doesn’t give back any useful value (void). A Promise is like a ticket for an operation that hasn’t finished yet;
// using async/await lets you write code that looks sequential while the program actually pauses
// and resumes when the operation is done.
export async function writeEntry(date: ISODate, content: string): Promise<void> {
// 1. This validates the input. If the date isn’t a correctly formatted real date,
// the function throws an error (stops immediately and reports a problem).
// The error message tells you what went wrong.
if (!isValidISODate(date)) {
// It creates a deliberate crash that can be caught and handled by whatever called this function.
throw new Error(`writeEntry: invalid date "${date}" — expected YYYY-MM-DD`);
}
// 2. await pauses execution until the promise returned by ensureDiaryDir() resolves.
// ensureDiaryDir() makes sure the diary folder exists. If it doesn’t, it creates it.
// This way, the very first time you save an entry, the necessary folder is created automatically.
await ensureDiaryDir();
// 3. entryPath(date) builds the full file path, e.g., "diary/2026-05-14.md".
const path = entryPath(date);
// 4. FileSystem.writeAsStringAsync(path, content) writes the entire content string to that file.
// If the file already existed, **its previous contents are completely replaced**.
// This is the “overwrite” behavior.
await FileSystem.writeAsStringAsync(path, content);
// 5. Logs a success message to the console (like a developer’s log).
// It shows how many characters were written—useful for debugging.
console.log(`[Diary] Wrote ${content.length} chars to ${date}.md`);
}
// ══════════════════════════════════════════════════════════════════════════════
// DELETE
// ══════════════════════════════════════════════════════════════════════════════
/**
* Deletes the diary file for a given date.
*
* If the file doesn’t exist, it does nothing (doesn’t crash).
* The designer wanted a no-op behavior (no operation) when the file is missing
* —similar to some systems’ “delete if exists, don’t complain if it’s not there.”
*
* @param date ISO date string "YYYY-MM-DD"
*
* @example
* await deleteEntry("2026-05-14");
*/
// Again, exported, asynchronous, takes a date string, returns nothing useful (void).
export async function deleteEntry(date: ISODate): Promise<void> {
// 1. If the date is invalid, it logs a warning (not an error) and then return;
// exits the function early without throwing.
// This is a gentler approach: it’s not a catastrophic failure,
// just a heads-up that something odd was passed.
if (!isValidISODate(date)) {
console.warn(`[Diary] deleteEntry: invalid date "${date}"`);
return;
}
// 2. entryPath(date) builds the full file path, e.g., "diary/2026-05-14.md".
const path = entryPath(date);
// 3. getInfoAsync(path) returns an object that includes an exists property (true or false), among other details.
// The program pauses until it gets this info.
const info = await FileSystem.getInfoAsync(path);
// 4. If the file doesn’t exist, log that fact and exit. No error, nothing to do.
if (!info.exists) {
console.log(`[Diary] deleteEntry: ${date}.md does not exist (no-op)`);
return;
}
// 5. deleteAsync removes the file.
// The option { idempotent: true } means “don’t throw an error if the file is already gone.”
// This is a safety net in case the file gets deleted by something else
// in the split second between checking info.exists and actually deleting it (a race condition).
// With this flag, even if the file vanished, the operation just succeeds silently.
await FileSystem.deleteAsync(path, { idempotent: true });
// 6. Logs successful deletion.
console.log(`[Diary] Deleted ${date}.md`);
}
// ══════════════════════════════════════════════════════════════════════════════
// LIST
// ══════════════════════════════════════════════════════════════════════════════
/**
* Strip Markdown formatting characters for use in a preview string.
*
* Converts:
* "# Heading" → "Heading"
* "**bold** text" → "bold text"
* "- list item" → "list item"
* "*italic*" → "italic"
*
* This is a lightweight, quick‑and‑dirty, regex-based stripper — good enough for a
* one-line preview. Takes a string and returns a cleaned‑up string.
*/
function stripMarkdown(text: string): string {
return text
.replace(/^#+\s*/gm, "") // "^", beginning of a line, heading markers "# ", "## ", etc. Replacement: empty string
.replace(/\*\*(.*?)\*\*/g, "$1") // bold **text**. Replacement: "$1".
// the first captured group (the text that was inside the asterisks). So **hello** becomes hello.
.replace(/\*(.*?)\*/g, "$1") // Same idea but for single asterisks (italic). *italic* → italic.
.replace(/^[-*]\s+/gm, "") // "^" – start of a line, "[-*]" – either a hyphen - or an asterisk *
// (common unordered list markers). "\s+" – one or more whitespace characters after the marker.
// Replacement: empty string – removes the list marker and its space, leaving just the list item content.
// Example: "* Buy milk" → "Buy milk".
.replace(/\n+/g, " ") // collapse newlines to spaces
// Pattern: \n+ – one or more newline characters. Replacement: a single space.
// This collapses line breaks, turning a multi-line preview into a single line of text.
.trim(); // Removes any leading or trailing whitespace from the final result.
}
/**
* List all diary entries, newest first.
*
* This function scans the folder where diary entries are stored,
* finds every file that looks like a date (e.g. 2026-05-14.md),
* reads a little bit of each file, and builds a list of summaries. Each summary contains:
* - date – the date string, like "2026-05-14"
* - wordCount – how many words are in the entry
* - preview – the first 100 characters of the entry, cleaned of Markdown symbols,
* so you can show a snippet in a list (like “Morning Reflection Grateful for…”)
*
* The list is sorted so the newest entry appears first.
*
* It does NOT read full file contents for entries other than computing
* the preview and word count — for a large diary (hundreds of entries)
* this still means reading every file once.
*
* @returns Array of DiaryEntrySummary, sorted by date descending
* (most recent first). Empty array if no entries exist yet.
*
* @example
* const entries = await listEntries();
* console.log(entries.length); // 3
* console.log(entries[0].date); // "2026-05-14" (most recent)
* console.log(entries[0].preview); // "Morning Reflection Grateful for..."
* console.log(entries[0].wordCount); // 42
*/
// The Function Signature
// export – makes the function usable in other files.
// async – tells the computer “this function uses operations that take time (like reading files),
// and I’ll use await inside to pause until they finish.”
// (): Promise – the function returns a Promise.
// A Promise is like a box that will eventually contain the result (or an error).
// Here the result is an array ([]) of DiaryEntrySummary objects.
// DiaryEntrySummary is just a description of the shape each entry has:
// a date, a word count, and a preview string.
// When you call listEntries(), you must use await (or .then()) to get the actual array.
export async function listEntries(): Promise<DiaryEntrySummary[]> {
// 1. Check if the diary folder exists
// DIARY_DIR is a constant (const DIARY_DIR = `${FileSystem.documentDirectory}diary/`;) that
// contains the path of the diary folder on the device.
// FileSystem.getInfoAsync returns an object describing that folder.
// The most important property is dirInfo.exists, which is true if the folder is there and false if it isn’t.
// await pauses the function here until the info is retrieved.
const dirInfo = await FileSystem.getInfoAsync(DIARY_DIR);
// If the folder doesn’t exist, it means no diary entries have ever been saved
// (the app has never run, or the folder was never created).
if (!dirInfo.exists) {
// We log a helpful message and return an empty array [] immediately —there’s nothing to list.
// This avoids errors from trying to read a missing folder.
console.log("[Diary] Diary directory does not exist yet (no entries).");
return [];
}
// 2. Read all filenames in the folder
// readDirectoryAsync returns an array of just the names of files and folders inside DIARY_DIR, not the full paths
// Example: ["2026-05-14.md", "2026-05-13.md", "notes.txt"]
const filenames = await FileSystem.readDirectoryAsync(DIARY_DIR);
// 3. Filter to only date‑like filenames
// .filter(...) goes through every item in the filenames array and keeps only those that pass a test.
// The test is a regular expression (regex): /^\d{4}-\d{2}-\d{2}\.md$/. Let’s decode it piece by piece:
// "^" – start of the string.
// "\d{4}" – exactly four digits (the year, e.g. 2026).
// "-" – a literal hyphen.
// "\d{2}" – exactly two digits (the month, e.g. 05).
// "-" – another hyphen.
// "\d{2}" – exactly two digits (the day, e.g. 14).
// "\.md" – a literal dot followed by md.
// The backslash escapes the dot because a normal dot in regex means “any character”.
// "$" – end of the string.
// So the whole pattern matches filenames exactly like "2026-05-14.md".
// Files like "notes.txt" or ".DS_Store" (a system file on some computers) are ignored.
// .test(name) returns true if the name matches, false otherwise.
// .filter() keeps only the names that return true.
// After this line, dateFilenames might be ["2026-05-14.md", "2026-05-13.md"].
const dateFilenames = filenames.filter((name) =>
/^\d{4}-\d{2}-\d{2}\.md$/.test(name)
);
// 4. Prepare an empty list for the summaries
// This creates an array (starting empty) where we’ll push each summary object
const entries: DiaryEntrySummary[] = [];
// 5. Loop over each date‑filename and build a summary
// This is a loop that runs once for each item in dateFilenames.
// Inside the loop, filename will be something like "2026-05-14.md".
for (const filename of dateFilenames) {
// a. Extract the date string
// .replace(...) removes the .md at the end by replacing it with an empty string.
// The regex /\.md$/ matches a literal .md only at the end of the string ($).
// So we get the clean date.
const date = filename.replace(/\.md$/, ""); // "2026-05-14.md" → "2026-05-14"
// b. Read the file’s contents
// try { ... } catch(e) { ... } is an error‑handling block.
// If any operation inside the try fails (e.g., the file can’t be read),
// the code jumps to the catch block instead of crashing the whole function.
try {
// readAsStringAsync reads the entire file and returns it as a big string.
// We give it the full path by combining DIARY_DIR and the filename (e.g. "/user/diary/" + "2026-05-14.md").
// The result is stored in content.
const content = await FileSystem.readAsStringAsync(`${DIARY_DIR}${filename}`);
// c. Calculate word count
// .trim() removes spaces, tabs, and newlines from the beginning and end of the string.
// If the file was completely empty (or only whitespace), trimmed will be "" (length 0).
const trimmed = content.trim();
// The word count is computed using a conditional (the ? : thing):
// If trimmed.length === 0 (the string is empty after trimming), set wordCount = 0.
// Otherwise, split the string at every chunk of one or more whitespace characters
// (\s+ in regex means “one or more spaces, tabs, or newlines”).
// This gives an array of words. Taking .length of that array gives the number of words.
// Example: "Hello world" → after trim same → split ["Hello", "world"] → length 2.
const wordCount = trimmed.length === 0
? 0
: trimmed.split(/\s+/).length;
// d. Create a preview string
// stripMarkdown is the helper we have talked about it earlier.
// It removes #, **, list markers, etc., so the text becomes plain.
// After stripping, .slice(0, 100) takes the first 100 characters of that cleaned text.
// This gives a short preview without reading the whole file
const preview = stripMarkdown(trimmed).slice(0, 100);
// e. Add the summary to the list
// .push(...) adds an object to the end of the entries array.
// The object has properties date, wordCount, and preview, which are set to the values we just computed.
entries.push({ date, wordCount, preview });
} catch (e) {
// f. Error handling inside the loop
// If reading the file or any step inside the try throws an error,
// we log the error with console.error and then do nothing else.
// The loop simply moves on to the next filename.
// This way one corrupt file won’t break the entire listing.
console.error(`[Diary] Failed to read ${filename} for listing:`, e);
}
}
// 6. Sort the summaries by date (newest first)
// .sort(...) rearranges the items in the array.
// Because our dates are ISO strings like "2026-05-14", string comparison works chronologically.
// For example, "2026-05-14" is greater than "2026-05-13" because '4' > '3' at the last differing character.
entries.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
// 7. Log and return
// A clever log message that says either “1 entry” or “N entries”
// (using a tiny inline conditional to pick “y” vs “ies”).
console.log(`[Diary] Listed ${entries.length} entr${entries.length === 1 ? "y" : "ies"}`);
// Finally, the function returns the array of summaries.
return entries;
}
// ══════════════════════════════════════════════════════════════════════════════
// UTILITY: get the diary directory path (for debugging / Settings screen)
// ══════════════════════════════════════════════════════════════════════════════
/**
* This is a simple getter that just returns the path of the diary folder (stored in the constant DIARY_DIR).
* It’s not async because it doesn’t wait for anything; it just returns a string immediately.
* It is useful for debugging (to see where the data is) or for a settings screen
* that might show the user the storage path.
*/
export function getDiaryDirectoryPath(): string {
return DIARY_DIR;
}
app/(tabs)/diary.tsx with the test screenThe file app/(tabs)/diary.tsx will eventually become the diary tab’s real screen — a rich editor where you write and save daily reflections. But before building that complicated UI, we want to verify that the diary storage layer (diaryService.ts) is rock solid.
// app/(tabs)/diary.tsx
//
// DIARY TAB test screen
//
// Tests diaryService.ts:
// 1. Write a test entry for today.
// 2. Read it back and check the content matches.
// 3. List all diary entries (should show the new one).
// 4. Delete the entry.
// 5. List again (should show it’s gone).
//
// It will be replaced with the full editor UI in future posts.
// useState is a React Hook that lets a component remember values between renders
import React, { useState } from "react";
// These are React Native building blocks:
// View – a container, like a DIV in HTML.
// Text – displays text.
// ScrollView – allows scrolling if content is taller than the screen.
// StyleSheet – a method to create organised styles (the s object).
// TouchableOpacity – a pressable area that becomes slightly transparent when touched, used as a button.
// ActivityIndicator – a spinning loading wheel.
import {
View, Text, ScrollView, StyleSheet,
TouchableOpacity, ActivityIndicator,
} from "react-native";
// readEntry, writeEntry, deleteEntry, listEntries, getDiaryDirectoryPath are the diary service functions being tested.
// They read/write/delete entries from the device’s filesystem and list all existing entries.
// getDiaryDirectoryPath just returns the folder path where diaries are stored.
import {
readEntry, writeEntry, deleteEntry, listEntries, getDiaryDirectoryPath,
} from "../../src/services/diaryService";
// todayISO is a utility that returns today’s date as a string in YYYY-MM-DD format (e.g., "2026-08-01").
import { todayISO } from "../../src/utils/dateUtils";
// A description of what a diary entry summary object looks like
// — it has fields like date, wordCount, and preview.
import type { DiaryEntrySummary } from "../../src/db/types";
// This defines the shape of every test log line. A log entry has:
interface LogEntry {
step: number; // – which step number it is (1, 2, 3, …)
label: string; // – a short description of what the step did.
ok: boolean; // – true if the step passed, false if it failed.
detail: string; // – extra information about the result.
}
// The Test Content Constant
// This is a sample Markdown string that the test writes into today’s diary entry.
const TEST_CONTENT =
"# Morning Reflection\n\n" +
"Today I am grateful for the gift of a new day.\n\n" +
"- A quiet moment to pray\n" +
"- Good health\n" +
"- The people I love\n\n" +
"**Lord**, guide my steps today.";
// Component State
// Inside DiaryScreen, three state variables are created with useState:
export default function DiaryScreen() {
// running (boolean, initially false). True while the test is in progress.
// It disables the button so you can’t start the test twice and shows a spinner.
const [running, setRunning] = useState(false);
// logs (LogEntry[], initially []). An array of log entries.
// Every time a step completes, a new entry is added here and React re‑renders the screen to show the new log.
const [logs, setLogs] = useState<LogEntry[]>([]);
// done (boolean, initially false). Set to true when the test finishes (whether successful or with an error).
// This triggers the final summary message to appear.
const [done, setDone] = useState(false);
// The runTest Function — Heart of the Test
// This is an asynchronous function (async function).
// That means it can use await to pause until a promise (like reading a file) finishes,
// without freezing the app.
async function runTest() {
// 1. Initial Setup
// Set running to true → the button becomes a spinner and is disabled.
setRunning(true);
// Reset done to false → any previous summary is hidden.
setDone(false);
// Create a local entries array to collect logs.
// This array is not React state yet;
// the log helper function pushes to it and then updates the state with a copy.
const entries: LogEntry[] = [];
// 2. The log Helper
// Every time a test step finishes, this function is called:
function log(step: number, label: string, ok: boolean, detail: string) {
// It appends a new LogEntry to the local entries array.
entries.push({ step, label, ok, detail });
// It calls setLogs([...entries]) to spread (copy) the array into new state.
// Using a fresh copy tells React that something changed, so the screen updates.
setLogs([...entries]);
}
// 3. Get Today’s Date
// This date is used as the filename (without extension) for the test entry.
const today = todayISO();
// 4. All the actual testing happens inside a try block.
// If any step throws an exception, the code jumps to the catch block.
try {
// Step 1: Write
// Saves the Markdown content into a file named today.md inside the diary directory.
// Logs success with the number of characters written.
await writeEntry(today, TEST_CONTENT);
log(1, "writeEntry(today, content)", true,
`Wrote ${TEST_CONTENT.length} chars to ${today}.md`);
// Step 2: Read Back and Verify
// Reads the file’s content.
const readBack = await readEntry(today);
// Compares readBack === TEST_CONTENT.
// If they match, the step passes; otherwise it fails and shows the length of the read‑back string.
const matches = readBack === TEST_CONTENT;
log(2, "readEntry(today) — verify content", matches,
matches ? "Content matches exactly" : `Mismatch! Got ${readBack.length} chars`);
// Step 3: List Entries
// Returns an array of entry summaries (each with date, wordCount, preview).
const list1 = await listEntries();
// Checks that the list length is at least 1, and
// that one of the entries has the same date as today.
const has1 = list1.length >= 1 && list1.some(e => e.date === today);
// Logs a summary of all entries found.
log(3, "listEntries() — shows today's entry", has1,
`${list1.length} entr${list1.length === 1 ? "y" : "ies"}: ` +
list1.map(e => `${e.date} (${e.wordCount} words)`).join(", "));
// Step 4: Preview Check
// Even though the real content is raw Markdown,
// the listEntries function generates a plain‑text preview (stripping Markdown syntax).
// It finds the entry for today in list1
const todayEntry = list1.find(e => e.date === today);
// It checks that its preview field contains the string "Morning Reflection".
// This verifies that Markdown stripping works correctly.
// todayEntry?.preview.includes("Morning Reflection") returns:
// true if the preview contains the text; false if it doesn’t; undefined if todayEntry is undefined
// !!undefined → false; !!true → true; !!false → false
// !! Convert whatever this expression returns into a clean boolean.
const previewOk = !!todayEntry?.preview.includes("Morning Reflection");
log(4, "preview strips Markdown correctly", previewOk,
`preview="${todayEntry?.preview}"`);
// Step 5: Delete the Entry
// Deletes the file today.md.
await deleteEntry(today);
// If it succeeds, logs a success message.
log(5, "deleteEntry(today)", true, `Deleted ${today}.md`);
// Step 6: Read After Delete
// After deletion, readEntry(today) should return an empty string ("").
const afterDelete = await readEntry(today);
// The step passes if the result is exactly "".
log(6, "readEntry(today) after delete — returns \"\"", afterDelete === "",
`length=${afterDelete.length}`);
// Step 7: List After Delete
// Gets the new list of entries.
const list2 = await listEntries();
// The test verifies that no entry in list2 has the date today.
// If that’s true, the step passes.
const gone = !list2.some(e => e.date === today);
log(7, "listEntries() — today's entry gone", gone,
`${list2.length} entr${list2.length === 1 ? "y" : "ies"} remaining`);
// Step 8: Delete Again (Idempotency)
// await deleteEntry(today); is called a second time.
await deleteEntry(today);
// The diary service is designed to not throw an error if the file doesn’t exist — it just quietly does nothing.
// This step confirms that behaviour. If no exception is thrown, the step passes.
log(8, "deleteEntry(today) again — no error (idempotent)", true,
"No exception thrown for already-deleted file");
} catch (e) {
// 5. Error Handling (Catch Block).
// If any of the steps throws an unexpected error (e.g., disk full, permission denied), the catch block runs.
// It pushes a special log entry with ok: false; label: "Unhandled error"; and the error message as detail.
// This ensures the test screen always shows what went wrong, and the app doesn’t crash.
entries.push({
step: entries.length + 1,
label: "Unhandled error",
ok: false,
detail: String(e),
});
setLogs([...entries]);
}
// 6. Cleanup
// After the try/catch, regardless of success or failure, the function sets:
setRunning(false);
setDone(true);
// This re‑enables the button (now showing “↺ Run again”) and makes the final summary appear.
}
// 7. The allPassed Calculation
// This is a simple variable that becomes true only when:
// (*) The test is finished (done is true), and
// (*) Every single log entry has ok === true.
const allPassed = done && logs.every(l => l.ok);
// 8. The Rendered UI
// The component returns a ScrollView that contains everything.
return (
// 8.1 Heading and Subtitle
// Displays the test title and explains what it does.
// The {"\n"} are line breaks.
// The actual folder path is shown dynamically using getDiaryDirectoryPath().
<ScrollView style={s.scroll} contentContainerStyle={s.content}>
<Text style={s.heading}>Diary Service Test</Text>
<Text style={s.sub}>
Write → Read → List → Delete cycle for diary entries.{"\n"}
Diary directory:{"\n"}{getDiaryDirectoryPath()}
</Text>
// 8.2 The Run Button
// When pressed, it calls runTest.
// While running is true, the button is visually disabled (grayed out) and can’t be pressed again.
<TouchableOpacity
style={[s.btn, running && s.btnDisabled]}
onPress={runTest}
disabled={running}
>
// Inside the button:
{running ? (
// If the test is running, an ActivityIndicator (spinner) is shown.
<ActivityIndicator color="#fff" size="small" />
) : (
// If the test isn’t running, the text changes depending on done:
// Before first run: “▶ Run diary test”
// After completion: “↺ Run again”
<Text style={s.btnText}>
{done ? "↺ Run again" : "▶ Run diary test"}
</Text>
)}
</TouchableOpacity>
// 8.3 The Log Entries
// For each log entry in the logs array, a styled row is created.
{logs.map((entry) => (
// The key is the step number to help React render efficiently.
// The row background is green (s.logOk) if the step passed, or red (s.logFail) if it failed.
<View key={entry.step} style={[s.logRow, entry.ok ? s.logOk : s.logFail]}>
// Inside each row:
<View style={s.logHeader}>
// (i) A header with an emoji (✅ for pass, ❌ for fail), the text "Step", and the step number.
<Text style={s.logStep}>{entry.ok ? "✅" : "❌"} Step {entry.step}</Text>
// (ii) The step’s label in bold, with colour matching the status.
<Text style={[s.logLabel, entry.ok ? s.textOk : s.textFail]}>
{entry.label}
</Text>
</View>
// (iii) A detail line in a smaller font showing extra info (like the character count or preview text).
<Text style={s.logDetail}>{entry.detail}</Text>
</View>
))}
// 8.4 Summary Banner
// After the test is finished (done is true), a summary box appears:
{done && (
<View style={[s.summary, allPassed ? s.summaryOk : s.summaryFail]}>
// If allPassed is true, the box is green and says “✅ All 8 steps passed — diary service working correctly.”
// It also shows a hint that Diary Service is complete and useDiary hook with auto-save is next.
// If any step failed, the box is red and says “❌ Some steps failed — check the Metro log.”
<Text style={allPassed ? s.textOk : s.textFail}>
{allPassed
? "✅ All 8 steps passed — diary service working correctly."
: "❌ Some steps failed — check the Metro log."}
</Text>
{allPassed && (
<Text style={s.summaryHint}>
{"\n"}Diary Service complete.{"\n"}
Next: useDiary hook with auto-save.
</Text>
)}
</View>
)}
</ScrollView>
);
}
const s = StyleSheet.create({
scroll: { flex: 1, backgroundColor: "#1a1f2e" },
content: { padding: 20, paddingBottom: 48 },
heading: { fontSize: 18, fontWeight: "bold", color: "#c9a84c", marginBottom: 4 },
sub: { fontSize: 11, color: "#6b7280", marginBottom: 20, lineHeight: 18,
fontFamily: "monospace" },
btn: { backgroundColor: "#5b8fd4", borderRadius: 8, padding: 14,
alignItems: "center", marginBottom: 20 },
btnDisabled: { backgroundColor: "#2d3550" },
btnText: { color: "#fff", fontSize: 15, fontWeight: "bold" },
logRow: { borderRadius: 8, padding: 12, marginBottom: 8, borderWidth: 1 },
logOk: { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
logFail: { backgroundColor: "#2e1a1a", borderColor: "#4a2d2d" },
logHeader: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 },
logStep: { fontSize: 13, color: "#dde4f0", fontWeight: "600" },
logLabel: { fontSize: 13, fontWeight: "600", flex: 1 },
logDetail: { fontSize: 11, color: "#6b7280", fontFamily: "monospace", lineHeight: 16 },
textOk: { color: "#86efac" },
textFail: { color: "#f87171" },
summary: { borderRadius: 10, padding: 16, marginTop: 8, borderWidth: 1 },
summaryOk: { backgroundColor: "#1a2e1a", borderColor: "#2d4a2d" },
summaryFail: { backgroundColor: "#2e1a1a", borderColor: "#4a2d2d" },
summaryHint: { fontSize: 12, color: "#4b5563", fontFamily: "monospace", lineHeight: 18 },
});
npx expo start --clear.✅ Step 1 writeEntry(today, content) Wrote 156 chars to 2026-05-14.md
✅ Step 2 readEntry(today) — verify content Content matches exactly
✅ Step 3 listEntries() — shows today's entry 1 entry: 2026-05-14 (24 words)
✅ Step 4 preview strips Markdown correctly preview="Morning Reflection Today I am grateful..."
✅ Step 5 deleteEntry(today) Deleted 2026-05-14.md
✅ Step 6 readEntry(today) after delete — "" length=0
✅ Step 7 listEntries() — today's entry gone 0 entries remaining
✅ Step 8 deleteEntry(today) again — idempotent No exception thrown
✅ All 8 steps passed — diary service working correctly.