diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.js
index 839997652f9c6b30f1c61a317d84f79821d3b77a..af5ba6acb3479b12794065e8d7bad81e47324ec7 100644
--- a/packages/desktop-client/e2e/page-models/navigation.js
+++ b/packages/desktop-client/e2e/page-models/navigation.js
@@ -1,4 +1,5 @@
 import { AccountPage } from './account-page';
+import { SchedulesPage } from './schedules-page';
 
 export class Navigation {
   constructor(page) {
@@ -12,4 +13,10 @@ export class Navigation {
 
     return new AccountPage(this.page);
   }
+
+  async goToSchedulesPage() {
+    await this.page.getByRole('link', { name: 'Schedules' }).click();
+
+    return new SchedulesPage(this.page);
+  }
 }
diff --git a/packages/desktop-client/e2e/page-models/schedules-page.js b/packages/desktop-client/e2e/page-models/schedules-page.js
new file mode 100644
index 0000000000000000000000000000000000000000..c61dbad381f3cb325659ab131dfb57df64ba78b7
--- /dev/null
+++ b/packages/desktop-client/e2e/page-models/schedules-page.js
@@ -0,0 +1,93 @@
+export class SchedulesPage {
+  constructor(page) {
+    this.page = page;
+
+    this.addNewScheduleButton = this.page.getByRole('button', {
+      name: 'Add new schedule',
+    });
+    this.schedulesTableRow = this.page.getByTestId('table').getByTestId('row');
+  }
+
+  /**
+   * Add a new schedule
+   */
+  async addNewSchedule(data) {
+    await this.addNewScheduleButton.click();
+
+    await this._fillScheduleFields(data);
+
+    await this.page.getByRole('button', { name: 'Add' }).click();
+  }
+
+  /**
+   * Retrieve the row element for the nth-schedule.
+   * 0-based index
+   */
+  getNthScheduleRow(index) {
+    return this.schedulesTableRow.nth(index);
+  }
+
+  /**
+   * Retrieve the data for the nth-schedule.
+   * 0-based index
+   */
+  async getNthSchedule(index) {
+    const row = this.getNthScheduleRow(index);
+
+    return {
+      payee: await row.getByTestId('payee').textContent(),
+      account: await row.getByTestId('account').textContent(),
+      date: await row.getByTestId('date').textContent(),
+      status: await row.getByTestId('status').textContent(),
+      amount: await row.getByTestId('amount').textContent(),
+    };
+  }
+
+  /**
+   * Create a transaction for the nth-schedule.
+   * 0-based index
+   */
+  async postNthSchedule(index) {
+    await this._performNthAction(index, 'Post transaction');
+    await this.page.waitForTimeout(1000);
+  }
+
+  /**
+   * Complete the nth-schedule.
+   * 0-based index
+   */
+  async completeNthSchedule(index) {
+    await this._performNthAction(index, 'Complete');
+    await this.page.waitForTimeout(1000);
+  }
+
+  async _performNthAction(index, actionName) {
+    const row = this.getNthScheduleRow(index);
+    const actions = row.getByTestId('actions');
+
+    await actions.getByRole('button').click();
+    await this.page.getByRole('button', { name: actionName }).click();
+  }
+
+  async _fillScheduleFields(data) {
+    if (data.payee) {
+      await this.page.getByLabel('Payee').click();
+      await this.page.keyboard.type(data.payee);
+      await this.page.keyboard.press('Enter');
+    }
+
+    if (data.account) {
+      await this.page.getByLabel('Account').click();
+      await this.page.keyboard.type(data.account);
+      await this.page.keyboard.press('Enter');
+    }
+
+    if (data.amount) {
+      await this.page.getByLabel('Amount').click();
+      await this.page.keyboard.press('Control+A');
+      await this.page.keyboard.press('Delete');
+      await this.page.keyboard.type(String(data.amount));
+      await this.page.keyboard.press('Enter');
+    }
+  }
+}
diff --git a/packages/desktop-client/e2e/schedules.test.js b/packages/desktop-client/e2e/schedules.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c194c3fb78a2af9d7b6df590aa091a49c385a04
--- /dev/null
+++ b/packages/desktop-client/e2e/schedules.test.js
@@ -0,0 +1,64 @@
+import { test, expect } from '@playwright/test';
+
+import { ConfigurationPage } from './page-models/configuration-page';
+import { Navigation } from './page-models/navigation';
+
+test.describe('Schedules', () => {
+  let page;
+  let navigation;
+  let schedulesPage;
+  let configurationPage;
+
+  test.beforeAll(async ({ browser }) => {
+    page = await browser.newPage();
+    navigation = new Navigation(page);
+    configurationPage = new ConfigurationPage(page);
+
+    await page.goto('/');
+    await configurationPage.createTestFile();
+  });
+
+  test.afterAll(async () => {
+    await page.close();
+  });
+
+  test.beforeEach(async () => {
+    schedulesPage = await navigation.goToSchedulesPage();
+  });
+
+  test('creates a new schedule, posts the transaction and later completes it', async () => {
+    await schedulesPage.addNewSchedule({
+      payee: 'Home Depot',
+      account: 'HSBC',
+      amount: 25,
+    });
+
+    expect(await schedulesPage.getNthSchedule(0)).toMatchObject({
+      payee: 'Home Depot',
+      account: 'HSBC',
+      amount: '~25.00',
+      status: 'Due',
+    });
+
+    await schedulesPage.postNthSchedule(0);
+    expect(await schedulesPage.getNthSchedule(0)).toMatchObject({
+      status: 'Paid',
+    });
+
+    // Go to transactions page
+    const accountPage = await navigation.goToAccountPage('HSBC');
+    expect(await accountPage.getNthTransaction(0)).toMatchObject({
+      payee: 'Home Depot',
+      category: 'Categorize',
+      debit: '25.00',
+      credit: '',
+    });
+
+    // Go back to schedules page
+    await navigation.goToSchedulesPage();
+    await schedulesPage.completeNthSchedule(0);
+    expect(await schedulesPage.getNthScheduleRow(0)).toHaveText(
+      'Show completed schedules',
+    );
+  });
+});
diff --git a/packages/desktop-client/src/components/schedules/EditSchedule.js b/packages/desktop-client/src/components/schedules/EditSchedule.js
index 7fe5ce68ab3b5f3f04a0db096568d78cf8c997e2..5e61e2373f2f7adc8df5f9e5690529a6a40070a8 100644
--- a/packages/desktop-client/src/components/schedules/EditSchedule.js
+++ b/packages/desktop-client/src/components/schedules/EditSchedule.js
@@ -427,10 +427,10 @@ export default function ScheduleDetails() {
     >
       <Stack direction="row" style={{ marginTop: 20 }}>
         <FormField style={{ flex: 1 }}>
-          <FormLabel title="Payee" />
+          <FormLabel title="Payee" htmlFor="payee-field" />
           <PayeeAutocomplete
             value={state.fields.payee}
-            inputProps={{ placeholder: '(none)' }}
+            inputProps={{ id: 'payee-field', placeholder: '(none)' }}
             onSelect={id =>
               dispatch({ type: 'set-field', field: 'payee', value: id })
             }
@@ -438,11 +438,11 @@ export default function ScheduleDetails() {
         </FormField>
 
         <FormField style={{ flex: 1 }}>
-          <FormLabel title="Account" />
+          <FormLabel title="Account" htmlFor="account-field" />
           <AccountAutocomplete
             includeClosedAccounts={false}
             value={state.fields.account}
-            inputProps={{ placeholder: '(none)' }}
+            inputProps={{ id: 'account-field', placeholder: '(none)' }}
             onSelect={id =>
               dispatch({ type: 'set-field', field: 'account', value: id })
             }
@@ -451,7 +451,11 @@ export default function ScheduleDetails() {
 
         <FormField style={{ flex: 1 }}>
           <Stack direction="row" align="center" style={{ marginBottom: 3 }}>
-            <FormLabel title="Amount" style={{ margin: 0, flex: 1 }} />
+            <FormLabel
+              title="Amount"
+              htmlFor="amount-field"
+              style={{ margin: 0, flex: 1 }}
+            />
             <OpSelect
               ops={['is', 'isapprox', 'isbetween']}
               value={state.fields.amountOp}
@@ -490,6 +494,7 @@ export default function ScheduleDetails() {
             />
           ) : (
             <AmountInput
+              id="amount-field"
               defaultValue={state.fields.amount}
               onChange={value =>
                 dispatch({
diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.js b/packages/desktop-client/src/components/schedules/SchedulesTable.js
index 9b02316cf176b95d8b13e3381f6e5acf23ca3ba1..770504e6c67721de2c62642f283fd617a5521ed2 100644
--- a/packages/desktop-client/src/components/schedules/SchedulesTable.js
+++ b/packages/desktop-client/src/components/schedules/SchedulesTable.js
@@ -95,6 +95,7 @@ export function ScheduleAmountCell({ amount, op }) {
         alignItems: 'center',
         padding: '0 5px',
       }}
+      name="amount"
     >
       {isApprox && (
         <View
@@ -206,18 +207,18 @@ export function SchedulesTable({
           ':hover': { backgroundColor: colors.hover },
         }}
       >
-        <Field width="flex">
+        <Field width="flex" name="payee">
           <DisplayId type="payees" id={item._payee} />
         </Field>
-        <Field width="flex">
+        <Field width="flex" name="account">
           <DisplayId type="accounts" id={item._account} />
         </Field>
-        <Field width={110}>
+        <Field width={110} name="date">
           {item.next_date
             ? monthUtils.format(item.next_date, dateFormat)
             : null}
         </Field>
-        <Field width={120} style={{ alignItems: 'flex-start' }}>
+        <Field width={120} name="status" style={{ alignItems: 'flex-start' }}>
           <StatusBadge status={statuses.get(item.id)} />
         </Field>
         <ScheduleAmountCell amount={item._amount} op={item._amountOp} />
@@ -229,7 +230,7 @@ export function SchedulesTable({
           </Field>
         )}
         {!minimal && (
-          <Field width={40}>
+          <Field width={40} name="actions">
             <OverflowMenu
               schedule={item}
               status={statuses.get(item.id)}
diff --git a/packages/desktop-client/src/components/util/AmountInput.js b/packages/desktop-client/src/components/util/AmountInput.js
index d21fe311af374520b1d4ff6e7bcb5ad20bd6fa3d..d9849a78a7dbf3571b3ca8b5134cf63afb6144c4 100644
--- a/packages/desktop-client/src/components/util/AmountInput.js
+++ b/packages/desktop-client/src/components/util/AmountInput.js
@@ -12,7 +12,7 @@ import {
 import Add from 'loot-design/src/svg/v1/Add';
 import Subtract from 'loot-design/src/svg/v1/Subtract';
 
-export function AmountInput({ defaultValue = 0, onChange, style }) {
+export function AmountInput({ id, defaultValue = 0, onChange, style }) {
   let [negative, setNegative] = useState(defaultValue <= 0);
   let [value, setValue] = useState(integerToCurrency(Math.abs(defaultValue)));
 
@@ -30,6 +30,7 @@ export function AmountInput({ defaultValue = 0, onChange, style }) {
 
   return (
     <InputWithContent
+      id={id}
       leftContent={
         <Button bare style={{ padding: '0 7px' }} onClick={onSwitch}>
           {negative ? (
diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js
index 876bd6275ca4945d99ef1a0b9f8603f9f9d728d2..cd98c800cb368376a1728a4a6bb708fa26f4ad47 100644
--- a/packages/loot-design/src/components/common.js
+++ b/packages/loot-design/src/components/common.js
@@ -626,6 +626,7 @@ export function Menu({ header, footer, items: allItems, onMenuSelect }) {
 
         return (
           <View
+            role="button"
             key={item.name}
             style={[
               {
diff --git a/packages/loot-design/src/components/forms.js b/packages/loot-design/src/components/forms.js
index 86d02bd8f6d4fe4a7481f7bb9cea469eaf99ea5a..50dbe703bfad247009a5522ca79892239722c897 100644
--- a/packages/loot-design/src/components/forms.js
+++ b/packages/loot-design/src/components/forms.js
@@ -25,10 +25,10 @@ export function SectionLabel({ title, style }) {
   );
 }
 
-export function FormLabel({ style, title }) {
+export function FormLabel({ style, title, htmlFor }) {
   return (
     <Text style={[{ fontSize: 13, marginBottom: 3, color: colors.n3 }, style]}>
-      {title}
+      <label htmlFor={htmlFor}>{title}</label>
     </Text>
   );
 }