Compare commits
57 Commits
copilot/fi
...
copilot/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c4f0fff58 | ||
|
|
acadee2a5c | ||
|
|
1a1c7447ad | ||
|
|
c8d7e4bc92 | ||
|
|
7b19600b29 | ||
|
|
9804625b57 | ||
|
|
10374316db | ||
|
|
bbd039e572 | ||
|
|
c5db712481 | ||
|
|
11837ddad5 | ||
|
|
0830540168 | ||
|
|
c22d445ae0 | ||
|
|
1cbacdd192 | ||
|
|
e678a69c70 | ||
|
|
6c75b4545d | ||
|
|
c42390ca9f | ||
|
|
ac8d247def | ||
|
|
34f0c6c2e7 | ||
|
|
63de281d13 | ||
|
|
751e886796 | ||
|
|
89d5b7b6ad | ||
|
|
c8aa0cf1d3 | ||
|
|
84cebed20b | ||
|
|
0c3b54ee7d | ||
|
|
eb9f9b3a73 | ||
|
|
5c31aa03ba | ||
|
|
266e7f9cac | ||
|
|
e951e21fe1 | ||
|
|
1a26253457 | ||
|
|
e72f18c5db | ||
|
|
5deb2cf790 | ||
|
|
111e01449d | ||
|
|
c0bd920c26 | ||
|
|
b695af66c0 | ||
|
|
650521f05b | ||
|
|
738a8cda7c | ||
|
|
deadd9aefc | ||
|
|
16ec636358 | ||
|
|
b271de32b6 | ||
|
|
2fb98156f6 | ||
|
|
2f86bafd1f | ||
|
|
7f6f4d5def | ||
|
|
c57260a504 | ||
|
|
1452ecfeb7 | ||
|
|
264cc9fb0e | ||
|
|
554d0b6150 | ||
|
|
11d0b9d824 | ||
|
|
323c2beb0a | ||
|
|
dc5ce6ae96 | ||
|
|
d8afc6b2be | ||
|
|
7732fac8b6 | ||
|
|
6da6f505e6 | ||
|
|
4674916d3e | ||
|
|
5388a115e9 | ||
|
|
06d31ce035 | ||
|
|
9585a92cda | ||
|
|
a0a490c14c |
57
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -8,35 +8,66 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
|
||||
|
||||
⚠️ **CRITICAL:** Bug reports without clear, step-by-step reproduction instructions will be closed. We cannot investigate or fix bugs without being able to reproduce them. Please take the time to provide detailed reproduction steps.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
|
||||
|
||||
**Bank-sync problems (SimpleFin / GoCardless)?** Reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) first and open an issue only if the community deems the issue to be a legitimate bug in Actual.
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
label: 'Verified issue does not already exist?'
|
||||
description: 'Please search to see if an issue already exists for the issue you encountered.'
|
||||
options:
|
||||
- label: 'I have searched and found no existing issue'
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen? If you're reporting an issue with imports, please attach a (redacted) version of the file you're having trouble importing. You may need to zip it before uploading.
|
||||
placeholder: Tell us what you see!
|
||||
value: 'A bug happened!'
|
||||
description: |
|
||||
Describe the bug clearly and concisely. Include:
|
||||
- What you were trying to do
|
||||
- What you expected to happen
|
||||
- What actually happened instead
|
||||
- Any error messages (copy/paste the exact text)
|
||||
|
||||
If you're reporting an issue with imports, please include a (redacted) version of the file, and a screenshot of the import screen. You may need to zip it before uploading.
|
||||
placeholder: |
|
||||
I was trying to [action] when [context].
|
||||
Expected: [expected behavior]
|
||||
Actual: [actual behavior]
|
||||
Error message: [if any]
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Reproduction Steps
|
||||
|
||||
**REQUIRED:** Without clear reproduction steps, we cannot investigate or fix the bug. Please provide detailed, step-by-step instructions that anyone can follow to reproduce the issue.
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How can we reproduce the issue?
|
||||
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
|
||||
value: 'How can we reproduce the issue?'
|
||||
description: |
|
||||
**This field is mandatory and must be filled out completely.**
|
||||
|
||||
Provide numbered, step-by-step instructions that allow us to reproduce the bug. Include:
|
||||
- Specific actions you took (e.g., "Click on the Budget tab", "Enter $100 in the amount field")
|
||||
- What you expected to happen
|
||||
- What actually happened instead
|
||||
|
||||
Example format:
|
||||
1. Navigate to [specific page/section]
|
||||
2. Click on [specific button/link]
|
||||
3. Enter [specific data] in [specific field]
|
||||
4. Click [action]
|
||||
5. Observe [expected vs actual behavior]
|
||||
|
||||
If the issue involves importing data, please attach a (redacted) sample file. You may need to zip it before uploading.
|
||||
placeholder: |
|
||||
1. Go to [specific location]
|
||||
2. Click [specific element]
|
||||
3. Enter [specific data]
|
||||
4. Click [action]
|
||||
5. Expected: [what should happen]
|
||||
Actual: [what actually happens]
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
|
||||
159
.github/scripts/count-points.mjs
vendored
@@ -8,6 +8,13 @@ const CONFIG = {
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
|
||||
PR_CONTRIBUTION_POINTS: {
|
||||
Features: 2,
|
||||
Enhancements: 2,
|
||||
Bugfix: 3,
|
||||
Maintenance: 2,
|
||||
Unknown: 2,
|
||||
},
|
||||
// Point tiers for code changes (non-docs)
|
||||
CODE_PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 500, points: 8 },
|
||||
@@ -31,6 +38,116 @@ const CONFIG = {
|
||||
DOCS_FILES_PATTERN: 'packages/docs/**/*',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse category from release notes file content.
|
||||
* @param {string} content - The content of the release notes file.
|
||||
* @returns {string|null} The category or null if not found.
|
||||
*/
|
||||
function parseReleaseNotesCategory(content) {
|
||||
if (!content) return null;
|
||||
|
||||
// Extract YAML front matter
|
||||
const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||
if (!frontMatterMatch) return null;
|
||||
|
||||
// Extract category from front matter
|
||||
const categoryMatch = frontMatterMatch[1].match(/^category:\s*(.+)$/m);
|
||||
if (!categoryMatch) return null;
|
||||
|
||||
return categoryMatch[1].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last commit SHA on or before a given date.
|
||||
* @param {Octokit} octokit - The Octokit instance.
|
||||
* @param {string} owner - Repository owner.
|
||||
* @param {string} repo - Repository name.
|
||||
* @param {Date} beforeDate - The date to find the last commit before.
|
||||
* @returns {Promise<string|null>} The commit SHA or null if not found.
|
||||
*/
|
||||
async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) {
|
||||
try {
|
||||
// Get the default branch from the repository
|
||||
const { data: repoData } = await octokit.repos.get({ owner, repo });
|
||||
const defaultBranch = repoData.default_branch;
|
||||
|
||||
const { data: commits } = await octokit.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
sha: defaultBranch,
|
||||
until: beforeDate.toISOString(),
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (commits.length > 0) {
|
||||
return commits[0].sha;
|
||||
}
|
||||
} catch {
|
||||
// If error occurs, return null to fall back to default branch
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category and points for a PR by reading its release notes file.
|
||||
* @param {Octokit} octokit - The Octokit instance.
|
||||
* @param {string} owner - Repository owner.
|
||||
* @param {string} repo - Repository name.
|
||||
* @param {number} prNumber - PR number.
|
||||
* @param {Date} monthEnd - The end date of the month to use as base revision.
|
||||
* @returns {Object} Object with category and points, or null if error.
|
||||
*/
|
||||
async function getPRCategoryAndPoints(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
prNumber,
|
||||
monthEnd,
|
||||
) {
|
||||
const releaseNotesPath = `upcoming-release-notes/${prNumber}.md`;
|
||||
|
||||
try {
|
||||
// Get the last commit of the month to use as base revision
|
||||
const commitSha = await getLastCommitBeforeDate(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
monthEnd,
|
||||
);
|
||||
|
||||
// Try to read the release notes file from the last commit of the month
|
||||
const { data: fileContent } = await octokit.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: releaseNotesPath,
|
||||
ref: commitSha || undefined, // Use commit SHA if available, otherwise default branch
|
||||
});
|
||||
|
||||
if (fileContent.content) {
|
||||
// Decode base64 content
|
||||
const content = Buffer.from(fileContent.content, 'base64').toString(
|
||||
'utf-8',
|
||||
);
|
||||
const category = parseReleaseNotesCategory(content);
|
||||
|
||||
if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) {
|
||||
return {
|
||||
category,
|
||||
points: CONFIG.PR_CONTRIBUTION_POINTS[category],
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return {
|
||||
category: 'Unknown',
|
||||
points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start and end dates for the last month.
|
||||
* @returns {Object} An object containing the start and end dates.
|
||||
@@ -89,6 +206,7 @@ async function countContributorPoints() {
|
||||
{
|
||||
codeReviews: [], // Will store objects with PR number and points for main repo changes
|
||||
docsReviews: [], // Will store objects with PR number and points for docs changes
|
||||
prContributions: [], // Will store objects with PR number, category, and points for PR author contributions
|
||||
labelRemovals: [],
|
||||
issueClosings: [],
|
||||
points: 0,
|
||||
@@ -202,6 +320,28 @@ async function countContributorPoints() {
|
||||
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
|
||||
}
|
||||
} else {
|
||||
// Award points to PR author if they are a core maintainer
|
||||
const prAuthor = pr.user?.login;
|
||||
if (prAuthor && orgMemberLogins.has(prAuthor)) {
|
||||
const categoryAndPoints = await getPRCategoryAndPoints(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
pr.number,
|
||||
until,
|
||||
);
|
||||
|
||||
if (categoryAndPoints) {
|
||||
const authorStats = stats.get(prAuthor);
|
||||
authorStats.prContributions.push({
|
||||
pr: pr.number.toString(),
|
||||
category: categoryAndPoints.category,
|
||||
points: categoryAndPoints.points,
|
||||
});
|
||||
authorStats.points += categoryAndPoints.points;
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueReviewers = new Set();
|
||||
reviews.data.forEach(review => {
|
||||
if (
|
||||
@@ -293,7 +433,7 @@ async function countContributorPoints() {
|
||||
// Print all statistics
|
||||
printStats(
|
||||
'Code Review Statistics',
|
||||
stats => stats.codeReviews.length,
|
||||
stats => stats.codeReviews.reduce((sum, r) => sum + r.points, 0),
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
@@ -308,7 +448,7 @@ async function countContributorPoints() {
|
||||
|
||||
printStats(
|
||||
'Docs Review Statistics',
|
||||
stats => stats.docsReviews.length,
|
||||
stats => stats.docsReviews.reduce((sum, r) => sum + r.points, 0),
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
@@ -316,16 +456,27 @@ async function countContributorPoints() {
|
||||
.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
'PR Contribution Statistics',
|
||||
stats => stats.prContributions.reduce((sum, r) => sum + r.points, 0),
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
.prContributions.map(r => `#${r.pr} (${r.points}pts - ${r.category})`)
|
||||
.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
'"Needs Triage" Label Removal Statistics',
|
||||
stats => stats.labelRemovals.length,
|
||||
stats => stats.labelRemovals.length * CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
|
||||
);
|
||||
|
||||
printStats(
|
||||
'Issue Closing Statistics',
|
||||
stats => stats.issueClosings.length,
|
||||
stats =>
|
||||
stats.issueClosings.length * CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
|
||||
);
|
||||
|
||||
1
.github/workflows/electron-master.yml
vendored
@@ -199,6 +199,7 @@ jobs:
|
||||
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
|
||||
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
|
||||
branch: 'release/${{ needs.build.outputs.version }}'
|
||||
draft: true
|
||||
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
|
||||
body: |
|
||||
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"typescript/no-var-requires": "warn",
|
||||
|
||||
// Import rules
|
||||
"import/consistent-type-specifier-style": "warn",
|
||||
"import/first": "error",
|
||||
"import/no-amd": "error",
|
||||
"import/no-default-export": "warn",
|
||||
@@ -111,7 +112,7 @@
|
||||
"import/no-duplicates": [
|
||||
"warn",
|
||||
{
|
||||
"prefer-inline": true
|
||||
"prefer-inline": false
|
||||
}
|
||||
],
|
||||
|
||||
@@ -119,7 +120,7 @@
|
||||
"react/exhaustive-deps": [
|
||||
"warn",
|
||||
{
|
||||
"additionalHooks": "(useQuery|useEffectAfterMount)"
|
||||
"additionalHooks": "(^useQuery$|^useEffectAfterMount$)"
|
||||
}
|
||||
],
|
||||
"react/jsx-curly-brace-presence": "warn",
|
||||
@@ -333,6 +334,10 @@
|
||||
"importNames": ["colors"],
|
||||
"message": "Please use themes instead of colors"
|
||||
},
|
||||
{
|
||||
"group": ["**/style/themes/*"],
|
||||
"message": "Please do not import theme files directly"
|
||||
},
|
||||
{
|
||||
"group": ["@actual-app/web/**/*"],
|
||||
"message": "Please do not import `@actual-app/web` in `loot-core`"
|
||||
@@ -385,6 +390,12 @@
|
||||
"typescript-paths/absolute-import": ["error", { "enableAlias": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/desktop-client/src/style/themes/*"],
|
||||
"rules": {
|
||||
"eslint/no-restricted-imports": "off"
|
||||
}
|
||||
},
|
||||
// TODO: enable these
|
||||
{
|
||||
"files": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import { type RuleEntity } from 'loot-core/types/models';
|
||||
import type { RuleEntity } from 'loot-core/types/models';
|
||||
|
||||
import * as api from './index';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const config: StorybookConfig = {
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
staticDirs: ['./public'],
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import('vite');
|
||||
|
||||
|
||||
9
packages/component-library/.storybook/public/_headers
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
# /assets folder contain processed assets with a file hash
|
||||
# They are safe for immutable caching, as filename change when content change
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public
|
||||
Cache-Control: max-age=365000000
|
||||
Cache-Control: immutable
|
||||
|
||||
139
packages/component-library/src/AlignedText.stories.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { AlignedText } from './AlignedText';
|
||||
|
||||
const meta = {
|
||||
title: 'AlignedText',
|
||||
component: AlignedText,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof AlignedText>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
left: 'Label',
|
||||
right: 'Value',
|
||||
style: { width: 300, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'AlignedText displays two pieces of content aligned on opposite sides.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TruncateLeft: Story = {
|
||||
args: {
|
||||
left: 'This is a very long label that should be truncated on the left side',
|
||||
right: '$100.00',
|
||||
truncate: 'left',
|
||||
style: { width: 250, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'When `truncate="left"`, the left content is truncated with ellipsis.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TruncateRight: Story = {
|
||||
args: {
|
||||
left: 'Short Label',
|
||||
right:
|
||||
'This is a very long value that should be truncated on the right side',
|
||||
truncate: 'right',
|
||||
style: { width: 250, display: 'flex' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'When `truncate="right"`, the right content is truncated with ellipsis.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FinancialAmount: Story = {
|
||||
args: {
|
||||
left: 'Groceries',
|
||||
right: '$1,234.56',
|
||||
style: { width: 300, display: 'flex' },
|
||||
rightStyle: { fontWeight: 'bold' },
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Example showing AlignedText used for displaying financial data.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomStyles: Story = {
|
||||
args: {
|
||||
left: 'Category',
|
||||
right: 'Amount',
|
||||
style: {
|
||||
width: 300,
|
||||
padding: 10,
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderRadius: 4,
|
||||
display: 'flex',
|
||||
},
|
||||
leftStyle: { color: '#666', fontStyle: 'italic' },
|
||||
rightStyle: { color: '#333', fontWeight: 'bold' },
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleRows: Story = {
|
||||
args: {
|
||||
left: 'Income',
|
||||
right: '$5,000.00',
|
||||
},
|
||||
render: () => (
|
||||
<div
|
||||
style={{ width: 300, display: 'flex', flexDirection: 'column', gap: 8 }}
|
||||
>
|
||||
<AlignedText
|
||||
left="Income"
|
||||
right="$5,000.00"
|
||||
rightStyle={{ color: 'green' }}
|
||||
style={{ display: 'flex' }}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Expenses"
|
||||
right="-$3,200.00"
|
||||
rightStyle={{ color: 'red' }}
|
||||
style={{ display: 'flex' }}
|
||||
/>
|
||||
<AlignedText
|
||||
left="Balance"
|
||||
right="$1,800.00"
|
||||
style={{ borderTop: '1px solid #ccc', paddingTop: 8, display: 'flex' }}
|
||||
rightStyle={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Multiple AlignedText components stacked to create a summary view.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
|
||||
import type { ComponentProps, CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import { Block } from './Block';
|
||||
import { View } from './View';
|
||||
|
||||
111
packages/component-library/src/Block.stories.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { Block } from './Block';
|
||||
import { theme } from './theme';
|
||||
|
||||
const meta = {
|
||||
title: 'Block',
|
||||
component: Block,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies Meta<typeof Block>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This is a Block component',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Block is a basic div wrapper that accepts Emotion CSS styles via the `style` prop.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export const WithStyles: Story = {
|
||||
args: {
|
||||
children: 'Styled Block',
|
||||
style: {
|
||||
padding: 20,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFlexLayout: Story = {
|
||||
render: () => (
|
||||
<Block
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 10,
|
||||
padding: 15,
|
||||
borderRadius: 4,
|
||||
color: theme.pageText,
|
||||
}}
|
||||
>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 1
|
||||
</Block>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 2
|
||||
</Block>
|
||||
<Block
|
||||
style={{
|
||||
padding: 10,
|
||||
backgroundColor: theme.cardBackground,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${theme.cardBorder}`,
|
||||
}}
|
||||
>
|
||||
Item 3
|
||||
</Block>
|
||||
</Block>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Block components can be nested and styled with flexbox.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AsContainer: Story = {
|
||||
args: {
|
||||
children: 'Container Block',
|
||||
style: {
|
||||
width: 300,
|
||||
padding: 25,
|
||||
textAlign: 'center',
|
||||
backgroundColor: theme.cardBackground,
|
||||
border: `2px dashed ${theme.cardBorder}`,
|
||||
borderRadius: 8,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type HTMLProps, type Ref } from 'react';
|
||||
import type { HTMLProps, Ref } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
|
||||
type BlockProps = HTMLProps<HTMLDivElement> & {
|
||||
innerRef?: Ref<HTMLDivElement>;
|
||||
|
||||
@@ -20,7 +20,6 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
|
||||
import { Button as ReactAriaButton } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
82
packages/component-library/src/Card.stories.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { Card } from './Card';
|
||||
import { Paragraph } from './Paragraph';
|
||||
import { theme } from './theme';
|
||||
|
||||
const meta = {
|
||||
title: 'Card',
|
||||
component: Card,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Card>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Card content goes here',
|
||||
style: {
|
||||
padding: 20,
|
||||
width: 300,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: `
|
||||
Default Card component uses the following theme CSS variables:
|
||||
- \`--color-cardBackground\`
|
||||
- \`--color-cardBorder\`
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomContent: Story = {
|
||||
args: {
|
||||
style: {
|
||||
padding: 20,
|
||||
width: 300,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
render: args => (
|
||||
<Card {...args}>
|
||||
<h3 style={{ ...styles.largeText }}>Card Title</h3>
|
||||
<Paragraph style={{ margin: 0 }}>
|
||||
This is a card with more complex content including a title and
|
||||
paragraph.
|
||||
</Paragraph>
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
|
||||
export const Narrow: Story = {
|
||||
args: {
|
||||
children: 'Narrow card',
|
||||
style: {
|
||||
padding: 15,
|
||||
width: 150,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Wide: Story = {
|
||||
args: {
|
||||
children: 'Wide card with more content space',
|
||||
style: {
|
||||
padding: 25,
|
||||
width: 500,
|
||||
color: theme.pageText,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { forwardRef, type ComponentProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
108
packages/component-library/src/ColorPicker.stories.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react';
|
||||
import { ColorSwatch } from 'react-aria-components';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
|
||||
const meta = {
|
||||
title: 'ColorPicker',
|
||||
component: ColorPicker,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
args: {
|
||||
onChange: fn(),
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof ColorPicker>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
defaultValue: '#690CB0',
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithColorSwatch: Story = {
|
||||
args: {
|
||||
defaultValue: '#1976D2',
|
||||
children: (
|
||||
<Button style={{ padding: 4 }}>
|
||||
<ColorSwatch
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomColorSet: Story = {
|
||||
args: {
|
||||
defaultValue: '#FF0000',
|
||||
columns: 4,
|
||||
colorset: [
|
||||
'#FF0000',
|
||||
'#00FF00',
|
||||
'#0000FF',
|
||||
'#FFFF00',
|
||||
'#FF00FF',
|
||||
'#00FFFF',
|
||||
'#FFA500',
|
||||
'#800080',
|
||||
],
|
||||
children: <Button>Custom Colors</Button>,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'ColorPicker with a custom color set and different column layout.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Controlled: Story = {
|
||||
args: {
|
||||
children: <Button>Pick a color</Button>,
|
||||
},
|
||||
render: () => {
|
||||
const [color, setColor] = useState('#388E3C');
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<ColorPicker value={color} onChange={c => setColor(c.toString('hex'))}>
|
||||
<Button style={{ padding: 4 }}>
|
||||
<ColorSwatch
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</ColorPicker>
|
||||
<span>Selected: {color}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Controlled ColorPicker with external state management.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ChangeEvent, type ReactNode } from 'react';
|
||||
import type { ChangeEvent, ReactNode } from 'react';
|
||||
import {
|
||||
ColorPicker as AriaColorPicker,
|
||||
ColorSwatch as AriaColorSwatch,
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
parseColor,
|
||||
type ColorPickerProps as AriaColorPickerProps,
|
||||
type ColorSwatchProps,
|
||||
} from 'react-aria-components';
|
||||
import type {
|
||||
ColorPickerProps as AriaColorPickerProps,
|
||||
ColorSwatchProps,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
90
packages/component-library/src/FormError.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { FormError } from './FormError';
|
||||
import { Input } from './Input';
|
||||
import { View } from './View';
|
||||
|
||||
const meta = {
|
||||
title: 'FormError',
|
||||
component: FormError,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof FormError>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This field is required',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'FormError displays validation error messages in red text.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InFormContext: Story = {
|
||||
render: () => (
|
||||
<View
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 5, width: 250 }}
|
||||
>
|
||||
<Input placeholder="Email address" style={{ borderColor: 'red' }} />
|
||||
<FormError>Please enter a valid email address</FormError>
|
||||
</View>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'FormError displayed below an input field with validation error.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleErrors: Story = {
|
||||
render: () => (
|
||||
<View style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<FormError>Password must be at least 8 characters</FormError>
|
||||
<FormError>Password must contain a number</FormError>
|
||||
<FormError>Password must contain a special character</FormError>
|
||||
</View>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Multiple FormError components for displaying several validation errors.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomStyle: Story = {
|
||||
args: {
|
||||
children: 'Custom styled error message',
|
||||
style: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
padding: 10,
|
||||
backgroundColor: '#ffebee',
|
||||
borderRadius: 4,
|
||||
border: '1px solid red',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongErrorMessage: Story = {
|
||||
args: {
|
||||
children:
|
||||
'This is a longer error message that explains the validation issue in more detail. Please correct the input and try again.',
|
||||
style: { maxWidth: 300 },
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type CSSProperties, type ReactNode } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ import {
|
||||
isValidElement,
|
||||
useEffect,
|
||||
useRef,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import type { ReactElement, Ref, RefObject } from 'react';
|
||||
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { forwardRef, type Ref } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
|
||||
type InlineFieldProps = {
|
||||
label: ReactNode;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, {
|
||||
type ChangeEvent,
|
||||
type ComponentPropsWithRef,
|
||||
type FocusEvent,
|
||||
type KeyboardEvent,
|
||||
import React from 'react';
|
||||
import type {
|
||||
ChangeEvent,
|
||||
ComponentPropsWithRef,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
} from 'react';
|
||||
import { Input as ReactAriaInput } from 'react-aria-components';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { forwardRef, type CSSProperties, type ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { Text } from './Text';
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
type CSSProperties,
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
type SVGProps,
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type {
|
||||
ComponentProps,
|
||||
ComponentType,
|
||||
CSSProperties,
|
||||
KeyboardEvent,
|
||||
ReactNode,
|
||||
SVGProps,
|
||||
} from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type HTMLProps } from 'react';
|
||||
import type { HTMLProps } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
|
||||
type ParagraphProps = HTMLProps<HTMLDivElement> & {
|
||||
style?: CSSProperties;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, type ComponentProps } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { Popover as ReactAriaPopover } from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState, type CSSProperties } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { SvgExpandArrow } from './icons/v0';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { type ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
import { View } from './View';
|
||||
|
||||
type SpaceBetweenProps = {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
type HTMLProps,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import type { HTMLProps, ReactNode, Ref } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
|
||||
type TextProps = HTMLProps<HTMLSpanElement> & {
|
||||
innerRef?: Ref<HTMLSpanElement>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ComponentProps } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { type CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { Tooltip as AriaTooltip, TooltipTrigger } from 'react-aria-components';
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { forwardRef, type HTMLProps, type Ref } from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import type { HTMLProps, Ref } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import type { CSSProperties } from './styles';
|
||||
|
||||
type ViewProps = HTMLProps<HTMLDivElement> & {
|
||||
className?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { type SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
import { css, keyframes } from '@emotion/css';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, type SVGProps } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
export const SvgLoading = (props: SVGProps<SVGSVGElement>) => {
|
||||
const { color = 'currentColor' } = props;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Config } from '@svgr/core';
|
||||
import type { Config } from '@svgr/core';
|
||||
|
||||
const tmpl: Config['template'] = (
|
||||
{ imports, interfaces, componentName, props, jsx },
|
||||
|
||||
@@ -12,8 +12,7 @@ const shadowLarge = {
|
||||
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export const styles: Record<string, any> = {
|
||||
export const styles: CSSProperties = {
|
||||
incomeHeaderHeight: 70,
|
||||
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
|
||||
monthRightPadding: 5,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// * Need to check to make sure if account exists when handling
|
||||
// * transaction changes in syncing
|
||||
|
||||
import { type Timestamp } from './timestamp';
|
||||
import type { Timestamp } from './timestamp';
|
||||
|
||||
/**
|
||||
* Represents a node within a trinary radix trie.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import murmurhash from 'murmurhash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { type TrieNode } from './merkle';
|
||||
import type { TrieNode } from './merkle';
|
||||
|
||||
/**
|
||||
* Hybrid Unique Logical Clock (HULC) timestamp generator
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { type AccountPage } from './page-models/account-page';
|
||||
import type { AccountPage } from './page-models/account-page';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { type MobileBankSyncPage } from './page-models/mobile-bank-sync-page';
|
||||
import type { MobileBankSyncPage } from './page-models/mobile-bank-sync-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
|
||||
test.describe('Mobile Bank Sync', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { type BankSyncPage } from './page-models/bank-sync-page';
|
||||
import type { BankSyncPage } from './page-models/bank-sync-page';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import * as monthUtils from 'loot-core/shared/months';
|
||||
import { amountToCurrency, currencyToAmount } from 'loot-core/shared/util';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { type MobileBudgetPage } from './page-models/mobile-budget-page';
|
||||
import type { MobileBudgetPage } from './page-models/mobile-budget-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
|
||||
const copyLastMonthBudget = async (
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,7 +1,7 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { type BudgetPage } from './page-models/budget-page';
|
||||
import type { BudgetPage } from './page-models/budget-page';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||
test.describe('Budget', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
"name": "Work",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"name": "Schedule Payee",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"payee_locations": [],
|
||||
@@ -167,6 +173,12 @@
|
||||
"hidden": false,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "6d28a243-3670-4c96-8334-216e31ea9468",
|
||||
"name": "Category Group",
|
||||
"hidden": false,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "F5751985-3290-41E7-B17F-6DBE979F315D",
|
||||
"name": "Bills",
|
||||
@@ -726,6 +738,30 @@
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"category_group_id": "6d28a243-3670-4c96-8334-216e31ea9468",
|
||||
"name": "Category",
|
||||
"hidden": false,
|
||||
"original_category_group_id": null,
|
||||
"note": null,
|
||||
"budgeted": 0,
|
||||
"activity": 0,
|
||||
"balance": 0,
|
||||
"goal_type": null,
|
||||
"goal_day": null,
|
||||
"goal_cadence": null,
|
||||
"goal_cadence_frequency": null,
|
||||
"goal_creation_month": null,
|
||||
"goal_target": 0,
|
||||
"goal_target_month": null,
|
||||
"goal_percentage_complete": null,
|
||||
"goal_months_to_budget": null,
|
||||
"goal_under_funded": null,
|
||||
"goal_overall_funded": null,
|
||||
"goal_overall_left": null,
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"months": [
|
||||
@@ -1674,7 +1710,8 @@
|
||||
"memo": "sending to savings",
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"flag_color": "blue",
|
||||
"flag_name": "Savings",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "8d3017e0-2aa6-4fe2-b011-c53c9f147eb6",
|
||||
"category_id": null,
|
||||
@@ -1694,7 +1731,8 @@
|
||||
"memo": null,
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"flag_color": "red",
|
||||
"flag_name": "One-off",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "2a20470a-634f-4efa-a7f6-f1c0b0bdda41",
|
||||
"category_id": "225be370-37da-4cf8-8b6b-4c6d61a0dd95",
|
||||
@@ -1714,7 +1752,8 @@
|
||||
"memo": "getting paid",
|
||||
"cleared": "reconciled",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"flag_color": "green",
|
||||
"flag_name": "JOB",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "c843e030-5a77-4dc5-9b93-f8acc64b74f8",
|
||||
"category_id": "36120d44-6c61-4402-985a-891a8d267858",
|
||||
@@ -1734,7 +1773,8 @@
|
||||
"memo": null,
|
||||
"cleared": "cleared",
|
||||
"approved": true,
|
||||
"flag_color": null,
|
||||
"flag_color": "purple",
|
||||
"flag_name": "Savings",
|
||||
"account_id": "125f339b-2a63-481e-84c0-f04d898905d2",
|
||||
"payee_id": "c843e030-5a77-4dc5-9b93-f8acc64b74f8",
|
||||
"category_id": "36120d44-6c61-4402-985a-891a8d267858",
|
||||
@@ -1870,8 +1910,245 @@
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"scheduled_transactions": [],
|
||||
"scheduled_subtransactions": []
|
||||
"scheduled_transactions": [
|
||||
{
|
||||
"id": "1db8beb8-ef31-4a07-b9a5-0648b1e3071a",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "every4Weeks",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every four weeks",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "2cb5676a-9b6e-4fff-aaf5-7ace218bb918",
|
||||
"date_first": "2025-08-03",
|
||||
"date_next": "2025-08-17",
|
||||
"frequency": "everyOtherWeek",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every other week",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "34157157-8ad5-46b4-aa67-36f2035478ce",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "everyOtherYear",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every other year",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "64a5e1ee-ac5f-4fd7-b955-818ed97c0886",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "every4Months",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every four months",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "6a77929b-a54f-4401-9fc0-e3be672fe946",
|
||||
"date_first": "2025-08-03",
|
||||
"date_next": "2025-08-18",
|
||||
"frequency": "twiceAMonth",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated twice a month",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "700739ce-35a2-4fb5-9522-70d152b73a81",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "monthly",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated monthly",
|
||||
"flag_color": "purple",
|
||||
"flag_name": "Savings",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "8afb5da0-e189-46bc-b41a-c3603588a950",
|
||||
"date_first": "2025-08-03",
|
||||
"date_next": "2025-08-10",
|
||||
"frequency": "weekly",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated weekly",
|
||||
"flag_color": "blue",
|
||||
"flag_name": "Savings",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "906ca596-9f93-4c73-aaf2-9ca1f8db8a86",
|
||||
"date_first": "2025-08-04",
|
||||
"date_next": "2025-08-04",
|
||||
"frequency": "never",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - not repeated",
|
||||
"flag_color": "red",
|
||||
"flag_name": "One-off",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "a06f9cef-ec00-4561-9546-22513e0e11bb",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "twiceAYear",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated twice a year",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "d5bf68a6-5026-47a8-a40f-7ecd2f9ba4da",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "yearly",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated yearly",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "e842e6b8-096f-4152-8acd-9566cbee293b",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "everyOtherMonth",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every other month",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "ec72c5af-00c9-4ea0-aaa8-6863471beea8",
|
||||
"date_first": "2025-08-05",
|
||||
"date_next": "2025-08-05",
|
||||
"frequency": "every3Months",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated every three months",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "f80e4d81-b640-4cac-a50c-39e6400e23a6",
|
||||
"date_first": "2025-08-03",
|
||||
"date_next": "2025-08-04",
|
||||
"frequency": "daily",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - repeated daily",
|
||||
"flag_color": null,
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "4b8f0a2e-9c7a-4f8e-9dcb-6a20b3d54f0e",
|
||||
"date_first": "2025-08-06",
|
||||
"date_next": "2025-09-06",
|
||||
"frequency": "monthly",
|
||||
"amount": -100000,
|
||||
"memo": "Scheduled - split categories monthly",
|
||||
"flag_color": "yellow",
|
||||
"flag_name": "Split",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": null,
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "6e9dcaa6-0e96-4b08-90f7-2f8f12b7e6b6",
|
||||
"date_first": "2025-08-07",
|
||||
"date_next": "2025-08-21",
|
||||
"frequency": "everyOtherWeek",
|
||||
"amount": -50000,
|
||||
"memo": "Scheduled - transfer to Saving",
|
||||
"flag_color": "orange",
|
||||
"flag_name": "Transfer",
|
||||
"account_id": "bc1d862f-bab0-41c3-bd1e-6cee8c688e32",
|
||||
"payee_id": "8d3017e0-2aa6-4fe2-b011-c53c9f147eb6",
|
||||
"category_id": null,
|
||||
"transfer_account_id": "125f339b-2a63-481e-84c0-f04d898905d2",
|
||||
"deleted": false
|
||||
}
|
||||
],
|
||||
"scheduled_subtransactions": [
|
||||
{
|
||||
"id": "2b5c23f6-109c-4f0f-8ee5-8b76407fc99f",
|
||||
"scheduled_transaction_id": "4b8f0a2e-9c7a-4f8e-9dcb-6a20b3d54f0e",
|
||||
"amount": -60000,
|
||||
"memo": "split part a",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "419ae801-27c8-424b-8f39-9611825803db",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
},
|
||||
{
|
||||
"id": "24a4b78f-5a83-4891-8205-b8bb3f9ddf34",
|
||||
"scheduled_transaction_id": "4b8f0a2e-9c7a-4f8e-9dcb-6a20b3d54f0e",
|
||||
"amount": -40000,
|
||||
"memo": "split part b",
|
||||
"payee_id": "0f0899e3-242f-42e6-aae9-a751060d878e",
|
||||
"category_id": "36120d44-6c61-4402-985a-891a8d267858",
|
||||
"transfer_account_id": null,
|
||||
"deleted": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"server_knowledge": 58
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect as baseExpect, type Locator } from '@playwright/test';
|
||||
import { expect as baseExpect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
export { test } from '@playwright/test';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { AccountPage } from './page-models/account-page';
|
||||
@@ -64,6 +64,32 @@ test.describe('Onboarding', () => {
|
||||
|
||||
await navigation.goToAccountPage('Saving');
|
||||
await expect(accountPage.accountBalance).toHaveText('250.00');
|
||||
|
||||
await navigation.goToSchedulesPage();
|
||||
const scheduleRows = page.getByTestId('table').getByTestId('row');
|
||||
const scheduleNames = [
|
||||
'Scheduled - repeated every four weeks',
|
||||
'Scheduled - repeated every other week',
|
||||
'Scheduled - repeated every other year',
|
||||
'Scheduled - repeated every four months',
|
||||
'Scheduled - repeated twice a month',
|
||||
'Scheduled - repeated monthly',
|
||||
'Scheduled - repeated weekly',
|
||||
'Scheduled - not repeated',
|
||||
'Scheduled - repeated twice a year',
|
||||
'Scheduled - repeated yearly',
|
||||
'Scheduled - repeated every other month',
|
||||
'Scheduled - repeated every three months',
|
||||
'Scheduled - repeated daily',
|
||||
'Scheduled - split categories monthly',
|
||||
'Scheduled - transfer to Saving',
|
||||
];
|
||||
|
||||
for (const scheduleName of scheduleNames) {
|
||||
await expect(scheduleRows.filter({ hasText: scheduleName })).toHaveCount(
|
||||
1,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('creates a new budget file by importing Actual budget', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { CloseAccountModal } from './close-account-modal';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export class BankSyncPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { AccountPage } from './account-page';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class CloseAccountModal {
|
||||
readonly locator: Locator;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { AccountPage } from './account-page';
|
||||
import { BudgetPage } from './budget-page';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class CustomReportPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
type ConditionsEntry = {
|
||||
field: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class BalanceMenuModal {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class MobileBankSyncPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class BudgetMenuModal {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
import { BalanceMenuModal } from './mobile-balance-menu-modal';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { EditNotesModal } from './mobile-edit-notes-modal';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class EditNotesModal {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class EnvelopeBudgetSummaryModal {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
import { MobileAccountsPage } from './mobile-accounts-page';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class MobilePayeesPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class MobileReportsPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class MobileRulesPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class MobileSchedulesPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class TrackingBudgetSummaryModal {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MobileAccountPage } from './mobile-account-page';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { AccountPage } from './account-page';
|
||||
import { BankSyncPage } from './bank-sync-page';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class PayeesPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { CustomReportPage } from './custom-report-page';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { EditRuleModal } from './edit-rule-modal';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
type ScheduleEntry = {
|
||||
scheduleName?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { ScheduleEditModal } from './schedule-edit-modal';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Locator, type Page } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class SettingsPage {
|
||||
readonly page: Page;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import { type MobilePayeesPage } from './page-models/mobile-payees-page';
|
||||
import type { MobilePayeesPage } from './page-models/mobile-payees-page';
|
||||
|
||||
test.describe('Mobile Payees', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import { type PayeesPage } from './page-models/payees-page';
|
||||
import type { PayeesPage } from './page-models/payees-page';
|
||||
|
||||
test.describe('Payees', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { type CustomReportPage } from './page-models/custom-report-page';
|
||||
import type { CustomReportPage } from './page-models/custom-report-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import { type ReportsPage } from './page-models/reports-page';
|
||||
import type { ReportsPage } from './page-models/reports-page';
|
||||
|
||||
test.describe.parallel('Reports', () => {
|
||||
let page: Page;
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import { type MobileRulesPage } from './page-models/mobile-rules-page';
|
||||
import type { MobileRulesPage } from './page-models/mobile-rules-page';
|
||||
|
||||
test.describe('Mobile Rules', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import { type RulesPage } from './page-models/rules-page';
|
||||
import type { RulesPage } from './page-models/rules-page';
|
||||
|
||||
test.describe('Rules', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
import { type MobileSchedulesPage } from './page-models/mobile-schedules-page';
|
||||
import type { MobileSchedulesPage } from './page-models/mobile-schedules-page';
|
||||
|
||||
test.describe('Mobile Schedules', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import { type SchedulesPage } from './page-models/schedules-page';
|
||||
import type { SchedulesPage } from './page-models/schedules-page';
|
||||
|
||||
test.describe('Schedules', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
import { type SettingsPage } from './page-models/settings-page';
|
||||
import type { SettingsPage } from './page-models/settings-page';
|
||||
|
||||
test.describe('Settings', () => {
|
||||
let page: Page;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
|
||||