Compare commits
185 Commits
mobile-vrt
...
loot-core-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ad10bd41d | ||
|
|
2edc6800ce | ||
|
|
22623ce65e | ||
|
|
c25e3d4163 | ||
|
|
339fac2806 | ||
|
|
2ebaa527be | ||
|
|
c5411518c4 | ||
|
|
36839ff153 | ||
|
|
9d6db12921 | ||
|
|
590ac1f95e | ||
|
|
8e76a65e0c | ||
|
|
c3eda4247e | ||
|
|
022b9b76b1 | ||
|
|
19f0037256 | ||
|
|
c626fc2f17 | ||
|
|
f523d25052 | ||
|
|
278ac0c730 | ||
|
|
0696c8113d | ||
|
|
688de5f604 | ||
|
|
881410bc74 | ||
|
|
b4d2d6a884 | ||
|
|
5cf439883e | ||
|
|
23bb89b96e | ||
|
|
7010ab1eb6 | ||
|
|
18f538c54b | ||
|
|
e170c0d274 | ||
|
|
dad702e5c2 | ||
|
|
224d445840 | ||
|
|
670419b087 | ||
|
|
58baf74992 | ||
|
|
d08be58f95 | ||
|
|
db68170cce | ||
|
|
1e1092e472 | ||
|
|
d1324408f4 | ||
|
|
9e478014c5 | ||
|
|
dd69e539d3 | ||
|
|
2cb668a40c | ||
|
|
3cefd98ce9 | ||
|
|
fa2830a1fd | ||
|
|
57ac062edc | ||
|
|
0c94214a8f | ||
|
|
2b72b2f2f2 | ||
|
|
985b653a87 | ||
|
|
f14b160e5c | ||
|
|
8eafa1e741 | ||
|
|
aefd9504bf | ||
|
|
1f6977da81 | ||
|
|
290402ee6a | ||
|
|
c3b95886db | ||
|
|
e53d444c32 | ||
|
|
c0f9073f35 | ||
|
|
19c6f85f5e | ||
|
|
d4f1f703ea | ||
|
|
914f59197f | ||
|
|
7c24c269e2 | ||
|
|
c52e5c856d | ||
|
|
b08756cc39 | ||
|
|
29fc22a171 | ||
|
|
815f69a051 | ||
|
|
83ceea4250 | ||
|
|
59d685fab6 | ||
|
|
a267e3abb5 | ||
|
|
e078ed21ba | ||
|
|
41d5922635 | ||
|
|
6f07894be7 | ||
|
|
871de93f2d | ||
|
|
15b2ef1591 | ||
|
|
1c05d7e5fe | ||
|
|
6666014fe5 | ||
|
|
dc425042ec | ||
|
|
59835a3ac1 | ||
|
|
b349edd9e0 | ||
|
|
f265dd9df0 | ||
|
|
a6da06a8ef | ||
|
|
f25dc1f261 | ||
|
|
5751d5d107 | ||
|
|
4b063450a4 | ||
|
|
fbb0f9bd75 | ||
|
|
6af0dbab56 | ||
|
|
5c94e3878e | ||
|
|
10ca29e1e9 | ||
|
|
4d89a9b86a | ||
|
|
34f3ccacf6 | ||
|
|
1b883aa0ab | ||
|
|
54054736e9 | ||
|
|
5cf170a442 | ||
|
|
f9eb017a54 | ||
|
|
15351e034e | ||
|
|
1895bc80c2 | ||
|
|
a91a8859ab | ||
|
|
a3256f5686 | ||
|
|
715bc00e3b | ||
|
|
4e07357221 | ||
|
|
03f2cabc18 | ||
|
|
259beb7665 | ||
|
|
0f3efde855 | ||
|
|
9aac44c58f | ||
|
|
0d9528e22c | ||
|
|
3f31d19d8a | ||
|
|
225c93914c | ||
|
|
c25e97b0f6 | ||
|
|
e775306f81 | ||
|
|
02824ad240 | ||
|
|
1a13e98f49 | ||
|
|
3d9e90f797 | ||
|
|
b253246fe2 | ||
|
|
778fc713f3 | ||
|
|
e0f0d8e241 | ||
|
|
310d299ebd | ||
|
|
130f357bab | ||
|
|
f89817170a | ||
|
|
ec37b39e34 | ||
|
|
23f75a6b6a | ||
|
|
f206ba2f0f | ||
|
|
bd5c0cb981 | ||
|
|
3635c8c88a | ||
|
|
5cb97d6f2f | ||
|
|
e8af5b9014 | ||
|
|
328196c485 | ||
|
|
644fe8bdc6 | ||
|
|
15b1b73379 | ||
|
|
8c7e93616f | ||
|
|
a56d6f9e05 | ||
|
|
75acfc79e1 | ||
|
|
300ddc6311 | ||
|
|
05dda5f9d7 | ||
|
|
37ad584826 | ||
|
|
f9c08a995d | ||
|
|
e37a42faf9 | ||
|
|
9f279486ce | ||
|
|
0b3155608c | ||
|
|
3301cfa2fd | ||
|
|
23de23bd4e | ||
|
|
79f640cbc0 | ||
|
|
f786bdcec3 | ||
|
|
f3ae31055e | ||
|
|
21cb684b26 | ||
|
|
e455369443 | ||
|
|
6d122c898d | ||
|
|
e6024f7a8b | ||
|
|
1485d9c871 | ||
|
|
85b3c5714e | ||
|
|
ce4b80f499 | ||
|
|
464d9878c6 | ||
|
|
71c208e444 | ||
|
|
1dce3183e5 | ||
|
|
051c8a6ed0 | ||
|
|
bdeb19424b | ||
|
|
5369494925 | ||
|
|
e653ad33a6 | ||
|
|
a7b8d1251c | ||
|
|
d5e0b7da5d | ||
|
|
279d545a28 | ||
|
|
0b6ea52d9b | ||
|
|
38c5f89c41 | ||
|
|
b774a3b216 | ||
|
|
dc5d1174c7 | ||
|
|
33a7524cd7 | ||
|
|
0a0e26372b | ||
|
|
a28fb93cec | ||
|
|
365da79783 | ||
|
|
df92c80c27 | ||
|
|
d0caf9f521 | ||
|
|
3f85aedd0b | ||
|
|
9b7a79a01c | ||
|
|
125510c981 | ||
|
|
327887b87d | ||
|
|
47ef916873 | ||
|
|
5064b06f2c | ||
|
|
4df03984bd | ||
|
|
92980ab55b | ||
|
|
3b97d1eec7 | ||
|
|
545c8d5456 | ||
|
|
f79edf866a | ||
|
|
83ea40dff9 | ||
|
|
444ac83697 | ||
|
|
8f725c7911 | ||
|
|
6725d56bb8 | ||
|
|
666b7870b7 | ||
|
|
686ce5b504 | ||
|
|
4373f4d8f9 | ||
|
|
479572fadb | ||
|
|
6e627c4e2e | ||
|
|
0f41e95952 | ||
|
|
7e889300ef |
@@ -161,7 +161,12 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
'no-with': 'warn',
|
'no-with': 'warn',
|
||||||
'no-whitespace-before-property': 'warn',
|
'no-whitespace-before-property': 'warn',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
additionalHooks: '(useQuery)',
|
||||||
|
},
|
||||||
|
],
|
||||||
'require-yield': 'warn',
|
'require-yield': 'warn',
|
||||||
'rest-spread-spacing': ['warn', 'never'],
|
'rest-spread-spacing': ['warn', 'never'],
|
||||||
strict: ['warn', 'never'],
|
strict: ['warn', 'never'],
|
||||||
|
|||||||
1
.github/workflows/electron-master.yml
vendored
@@ -40,6 +40,7 @@ jobs:
|
|||||||
python3 -m pip install setuptools
|
python3 -m pip install setuptools
|
||||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||||
run: |
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install flatpak -y
|
sudo apt-get install flatpak -y
|
||||||
sudo apt-get install flatpak-builder -y
|
sudo apt-get install flatpak-builder -y
|
||||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|||||||
1
.github/workflows/electron-pr.yml
vendored
@@ -35,6 +35,7 @@ jobs:
|
|||||||
python3 -m pip install setuptools
|
python3 -m pip install setuptools
|
||||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||||
run: |
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
sudo apt-get install flatpak -y
|
sudo apt-get install flatpak -y
|
||||||
sudo apt-get install flatpak-builder -y
|
sudo apt-get install flatpak-builder -y
|
||||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|||||||
13
.github/workflows/size-compare.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for ${{github.base_ref}} build to succeed
|
- name: Wait for ${{github.base_ref}} build to succeed
|
||||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||||
id: master-build
|
id: master-build
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
ref: ${{github.base_ref}}
|
ref: ${{github.base_ref}}
|
||||||
|
|
||||||
- name: Wait for PR build to succeed
|
- name: Wait for PR build to succeed
|
||||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||||
id: wait-for-build
|
id: wait-for-build
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||||
exit 1
|
exit 1
|
||||||
- name: Download build artifact from ${{github.base_ref}}
|
- name: Download build artifact from ${{github.base_ref}}
|
||||||
uses: dawidd6/action-download-artifact@v3
|
uses: dawidd6/action-download-artifact@v6
|
||||||
id: pr-build
|
id: pr-build
|
||||||
with:
|
with:
|
||||||
branch: ${{github.base_ref}}
|
branch: ${{github.base_ref}}
|
||||||
@@ -55,12 +55,13 @@ jobs:
|
|||||||
path: base
|
path: base
|
||||||
|
|
||||||
- name: Download build artifact from PR
|
- name: Download build artifact from PR
|
||||||
uses: dawidd6/action-download-artifact@v3
|
uses: dawidd6/action-download-artifact@v6
|
||||||
with:
|
with:
|
||||||
pr: ${{github.event.pull_request.number}}
|
pr: ${{github.event.pull_request.number}}
|
||||||
workflow: build.yml
|
workflow: build.yml
|
||||||
name: build-stats
|
name: build-stats
|
||||||
path: head
|
path: head
|
||||||
|
allow_forks: true
|
||||||
|
|
||||||
- name: Strip content hashes from stats files
|
- name: Strip content hashes from stats files
|
||||||
run: |
|
run: |
|
||||||
@@ -70,14 +71,14 @@ jobs:
|
|||||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
||||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
||||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
|
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
|
||||||
- uses: twk3/rollup-size-compare-action@v1.0.0
|
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
current-stats-json-path: ./head/web-stats.json
|
current-stats-json-path: ./head/web-stats.json
|
||||||
base-stats-json-path: ./base/web-stats.json
|
base-stats-json-path: ./base/web-stats.json
|
||||||
title: desktop-client
|
title: desktop-client
|
||||||
|
|
||||||
- uses: github/webpack-bundlesize-compare-action@v1.8.2
|
- uses: github/webpack-bundlesize-compare-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
current-stats-json-path: ./head/loot-core-stats.json
|
current-stats-json-path: ./head/loot-core-stats.json
|
||||||
|
|||||||
113
.github/workflows/update-vrt.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
name: /update-vrt
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [ created ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-vrt:
|
||||||
|
name: Update VRT
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event.issue.pull_request &&
|
||||||
|
contains(github.event.comment.body, '/update-vrt')
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||||
|
steps:
|
||||||
|
- name: Get PR branch
|
||||||
|
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||||
|
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||||
|
id: comment-branch
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||||
|
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||||
|
- name: Set up environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Wait for Netlify build to finish
|
||||||
|
id: netlify
|
||||||
|
env:
|
||||||
|
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./.github/actions/netlify-wait-for-build
|
||||||
|
- name: Run VRT Tests on Netlify URL
|
||||||
|
run: yarn vrt --update-snapshots
|
||||||
|
env:
|
||||||
|
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||||
|
- name: Create patch
|
||||||
|
run: |
|
||||||
|
git config --system --add safe.directory "*"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git reset
|
||||||
|
git add "**/*.png"
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "Update VRT"
|
||||||
|
git format-patch -1 HEAD --stdout > Update-VRT.patch
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: patch
|
||||||
|
path: Update-VRT.patch
|
||||||
|
|
||||||
|
push-patch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: update-vrt
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Get PR branch
|
||||||
|
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||||
|
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||||
|
id: comment-branch
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||||
|
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
name: patch
|
||||||
|
- name: Apply patch and push
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git apply Update-VRT.patch
|
||||||
|
git add "**/*.png"
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "Update VRT"
|
||||||
|
git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }}
|
||||||
|
- name: Add finished reaction
|
||||||
|
uses: dkershner6/reaction-action@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commentId: ${{ github.event.comment.id }}
|
||||||
|
reaction: "rocket"
|
||||||
|
|
||||||
|
add-starting-reaction:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event.issue.pull_request &&
|
||||||
|
contains(github.event.comment.body, '/update-vrt')
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: React to comment
|
||||||
|
uses: dkershner6/reaction-action@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commentId: ${{ github.event.comment.id }}
|
||||||
|
reaction: "+1"
|
||||||
3
.gitignore
vendored
@@ -50,3 +50,6 @@ bundle.mobile.js.map
|
|||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
|
||||||
|
# build output
|
||||||
|
package.tgz
|
||||||
|
|||||||
1
TODO.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Figure out why loot-core-server is not detecting loot-core-shared files.
|
||||||
@@ -4,7 +4,8 @@ ROOT=`dirname $0`
|
|||||||
|
|
||||||
cd "$ROOT/.."
|
cd "$ROOT/.."
|
||||||
|
|
||||||
yarn workspace loot-core build:browser
|
yarn workspace loot-core-server build:browser
|
||||||
|
# yarn workspace loot-core build:browser
|
||||||
yarn workspace @actual-app/web build:browser
|
yarn workspace @actual-app/web build:browser
|
||||||
|
|
||||||
echo "packages/desktop-client/build"
|
echo "packages/desktop-client/build"
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ if [ "$OSTYPE" == "msys" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
yarn workspace loot-core build:node
|
yarn workspace loot-core-server build:node
|
||||||
|
# yarn workspace loot-core build:node
|
||||||
|
|
||||||
yarn workspace @actual-app/web build --mode=desktop
|
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||||
|
|
||||||
yarn workspace desktop-electron update-client
|
yarn workspace desktop-electron update-client
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actual-app/api",
|
"name": "@actual-app/api",
|
||||||
"version": "6.10.0",
|
"version": "24.11.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "An API for Actual",
|
"description": "An API for Actual",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
1
packages/desktop-client/.gitignore
vendored
@@ -10,6 +10,7 @@ playwright-report
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
build
|
build
|
||||||
|
build-electron
|
||||||
build-stats
|
build-stats
|
||||||
stats.json
|
stats.json
|
||||||
|
|
||||||
|
|||||||
64
packages/desktop-client/e2e/accounts.mobile.test.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { ConfigurationPage } from './page-models/configuration-page';
|
||||||
|
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||||
|
|
||||||
|
test.describe('Mobile Accounts', () => {
|
||||||
|
let page;
|
||||||
|
let navigation;
|
||||||
|
let configurationPage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ browser }) => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
navigation = new MobileNavigation(page);
|
||||||
|
configurationPage = new ConfigurationPage(page);
|
||||||
|
|
||||||
|
await page.setViewportSize({
|
||||||
|
width: 350,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
await page.goto('/');
|
||||||
|
await configurationPage.createTestFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens the accounts page and asserts on balances', async () => {
|
||||||
|
const accountsPage = await navigation.goToAccountsPage();
|
||||||
|
await accountsPage.waitFor();
|
||||||
|
|
||||||
|
const account = await accountsPage.getNthAccount(1);
|
||||||
|
|
||||||
|
await expect(account.name).toHaveText('Ally Savings');
|
||||||
|
await expect(account.balance).toHaveText('7,653.00');
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens individual account page and checks that filtering is working', async () => {
|
||||||
|
const accountsPage = await navigation.goToAccountsPage();
|
||||||
|
await accountsPage.waitFor();
|
||||||
|
|
||||||
|
const accountPage = await accountsPage.openNthAccount(0);
|
||||||
|
await accountPage.waitFor();
|
||||||
|
|
||||||
|
await expect(accountPage.heading).toHaveText('Bank of America');
|
||||||
|
await expect(accountPage.transactionList).toBeVisible();
|
||||||
|
await expect(await accountPage.getBalance()).toBeGreaterThan(0);
|
||||||
|
await expect(accountPage.noTransactionsMessage).not.toBeVisible();
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
|
await accountPage.searchByText('nothing should be found');
|
||||||
|
await expect(accountPage.noTransactionsMessage).toBeVisible();
|
||||||
|
await expect(accountPage.transactions).toHaveCount(0);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
|
await accountPage.clearSearch();
|
||||||
|
await expect(accountPage.transactions).not.toHaveCount(0);
|
||||||
|
|
||||||
|
await accountPage.searchByText('Kroger');
|
||||||
|
await expect(accountPage.transactions).not.toHaveCount(0);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
import { ConfigurationPage } from './page-models/configuration-page';
|
import { ConfigurationPage } from './page-models/configuration-page';
|
||||||
@@ -9,7 +11,7 @@ test.describe('Accounts', () => {
|
|||||||
let configurationPage;
|
let configurationPage;
|
||||||
let accountPage;
|
let accountPage;
|
||||||
|
|
||||||
test.beforeAll(async ({ browser }) => {
|
test.beforeEach(async ({ browser }) => {
|
||||||
page = await browser.newPage();
|
page = await browser.newPage();
|
||||||
navigation = new Navigation(page);
|
navigation = new Navigation(page);
|
||||||
configurationPage = new ConfigurationPage(page);
|
configurationPage = new ConfigurationPage(page);
|
||||||
@@ -18,7 +20,7 @@ test.describe('Accounts', () => {
|
|||||||
await configurationPage.createTestFile();
|
await configurationPage.createTestFile();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterEach(async () => {
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,6 +62,8 @@ test.describe('Accounts', () => {
|
|||||||
|
|
||||||
test('creates a transfer from two existing transactions', async () => {
|
test('creates a transfer from two existing transactions', async () => {
|
||||||
accountPage = await navigation.goToAccountPage('For budget');
|
accountPage = await navigation.goToAccountPage('For budget');
|
||||||
|
await accountPage.waitFor();
|
||||||
|
|
||||||
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');
|
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');
|
||||||
|
|
||||||
await accountPage.filterByNote('Test Acc Transfer');
|
await accountPage.filterByNote('Test Acc Transfer');
|
||||||
@@ -99,4 +103,61 @@ test.describe('Accounts', () => {
|
|||||||
await expect(transaction.account).toHaveText('Ally Savings');
|
await expect(transaction.account).toHaveText('Ally Savings');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Import Transactions', () => {
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
accountPage = await navigation.createAccount({
|
||||||
|
name: 'CSV import',
|
||||||
|
offBudget: false,
|
||||||
|
balance: 0,
|
||||||
|
});
|
||||||
|
await accountPage.waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function importCsv(screenshot = false) {
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await accountPage.page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
||||||
|
|
||||||
|
if (screenshot) await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
|
const importButton = accountPage.page.getByRole('button', {
|
||||||
|
name: /Import \d+ transactions/,
|
||||||
|
});
|
||||||
|
await importButton.click();
|
||||||
|
|
||||||
|
await expect(importButton).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('imports transactions from a CSV file', async () => {
|
||||||
|
await importCsv(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('import csv file twice', async () => {
|
||||||
|
await importCsv(false);
|
||||||
|
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await accountPage.page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
||||||
|
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
|
||||||
|
const importButton = accountPage.page.getByRole('button', {
|
||||||
|
name: /Import \d+ transactions/,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(importButton).toBeDisabled();
|
||||||
|
await expect(await importButton.innerText()).toMatch(
|
||||||
|
/Import 0 transactions/,
|
||||||
|
);
|
||||||
|
|
||||||
|
await accountPage.page.getByRole('button', { name: 'Close' }).click();
|
||||||
|
|
||||||
|
await expect(importButton).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
280
packages/desktop-client/e2e/budget.mobile.test.js
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import * as monthUtils from 'loot-core-shared/months';
|
||||||
|
|
||||||
|
import { ConfigurationPage } from './page-models/configuration-page';
|
||||||
|
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||||
|
|
||||||
|
const budgetTypes = ['Envelope', 'Tracking'];
|
||||||
|
|
||||||
|
budgetTypes.forEach(budgetType => {
|
||||||
|
test.describe(`Mobile Budget [${budgetType}]`, () => {
|
||||||
|
let page;
|
||||||
|
let navigation;
|
||||||
|
let configurationPage;
|
||||||
|
let previousGlobalIsTesting;
|
||||||
|
|
||||||
|
test.beforeAll(() => {
|
||||||
|
// TODO: Hack, properly mock the currentMonth function
|
||||||
|
previousGlobalIsTesting = global.IS_TESTING;
|
||||||
|
global.IS_TESTING = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(() => {
|
||||||
|
// TODO: Hack, properly mock the currentMonth function
|
||||||
|
global.IS_TESTING = previousGlobalIsTesting;
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async ({ browser }) => {
|
||||||
|
page = await browser.newPage();
|
||||||
|
navigation = new MobileNavigation(page);
|
||||||
|
configurationPage = new ConfigurationPage(page);
|
||||||
|
|
||||||
|
await page.setViewportSize({
|
||||||
|
width: 350,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
await page.goto('/');
|
||||||
|
await configurationPage.createTestFile();
|
||||||
|
|
||||||
|
if (budgetType === 'Tracking') {
|
||||||
|
// Set budget type to tracking
|
||||||
|
const settingsPage = await navigation.goToSettingsPage();
|
||||||
|
await settingsPage.useBudgetType('tracking');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads the budget page with budgeted amounts', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
await expect(budgetPage.categoryNames).toHaveText([
|
||||||
|
'Food',
|
||||||
|
'Restaurants',
|
||||||
|
'Entertainment',
|
||||||
|
'Clothing',
|
||||||
|
'General',
|
||||||
|
'Gift',
|
||||||
|
'Medical',
|
||||||
|
'Savings',
|
||||||
|
'Cell',
|
||||||
|
'Internet',
|
||||||
|
'Mortgage',
|
||||||
|
'Water',
|
||||||
|
'Power',
|
||||||
|
'Starting Balances',
|
||||||
|
'Misc',
|
||||||
|
'Income',
|
||||||
|
]);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Page Header Tests
|
||||||
|
|
||||||
|
test('checks that clicking the Actual logo in the page header opens the budget page menu', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
await budgetPage.openBudgetPageMenu();
|
||||||
|
|
||||||
|
const budgetPageMenuModal = page.getByRole('dialog');
|
||||||
|
|
||||||
|
await expect(budgetPageMenuModal).toBeVisible();
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checks that clicking the left arrow in the page header shows the previous month's budget", async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const selectedMonth = await budgetPage.getSelectedMonth();
|
||||||
|
const displayMonth = monthUtils.format(
|
||||||
|
selectedMonth,
|
||||||
|
budgetPage.MONTH_HEADER_DATE_FORMAT,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(budgetPage.heading).toHaveText(displayMonth);
|
||||||
|
|
||||||
|
const previousMonth = await budgetPage.goToPreviousMonth();
|
||||||
|
const previousDisplayMonth = monthUtils.format(
|
||||||
|
previousMonth,
|
||||||
|
budgetPage.MONTH_HEADER_DATE_FORMAT,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(budgetPage.heading).toHaveText(previousDisplayMonth);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checks that clicking the month in the page header opens the month menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const selectedMonth = await budgetPage.getSelectedMonth();
|
||||||
|
|
||||||
|
await budgetPage.openMonthMenu();
|
||||||
|
|
||||||
|
const monthMenuModal = page.getByRole('dialog');
|
||||||
|
const monthMenuModalHeading = monthMenuModal.getByRole('heading');
|
||||||
|
|
||||||
|
const displayMonth = monthUtils.format(
|
||||||
|
selectedMonth,
|
||||||
|
budgetPage.MONTH_HEADER_DATE_FORMAT,
|
||||||
|
);
|
||||||
|
await expect(monthMenuModalHeading).toHaveText(displayMonth);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checks that clicking the right arrow in the page header shows the next month's budget", async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const selectedMonth = await budgetPage.getSelectedMonth();
|
||||||
|
const displayMonth = monthUtils.format(
|
||||||
|
selectedMonth,
|
||||||
|
budgetPage.MONTH_HEADER_DATE_FORMAT,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(budgetPage.heading).toHaveText(displayMonth);
|
||||||
|
|
||||||
|
const nextMonth = await budgetPage.goToNextMonth();
|
||||||
|
const nextDisplayMonth = monthUtils.format(
|
||||||
|
nextMonth,
|
||||||
|
budgetPage.MONTH_HEADER_DATE_FORMAT,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(budgetPage.heading).toHaveText(nextDisplayMonth);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Category / Category Group Menu Tests
|
||||||
|
|
||||||
|
test('checks that clicking the category group name opens the category group menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryGroupName = await budgetPage.getCategoryGroupNameForRow(0);
|
||||||
|
await budgetPage.openCategoryGroupMenu(categoryGroupName);
|
||||||
|
|
||||||
|
const categoryMenuModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(categoryMenuModalHeading).toHaveText(categoryGroupName);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('checks that clicking the category name opens the category menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryName = await budgetPage.getCategoryNameForRow(0);
|
||||||
|
await budgetPage.openCategoryMenu(categoryName);
|
||||||
|
|
||||||
|
const categoryMenuModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(categoryMenuModalHeading).toHaveText(categoryName);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Budgeted Cell Tests
|
||||||
|
|
||||||
|
test('checks that clicking the budgeted cell opens the budget menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryName = await budgetPage.getCategoryNameForRow(0);
|
||||||
|
await budgetPage.openBudgetMenu(categoryName);
|
||||||
|
|
||||||
|
const budgetMenuModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(budgetMenuModalHeading).toHaveText(categoryName);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates the budgeted amount', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryName = await budgetPage.getCategoryNameForRow(0);
|
||||||
|
|
||||||
|
// Set to 100.00
|
||||||
|
await budgetPage.setBudget(categoryName, 10000);
|
||||||
|
|
||||||
|
const budgetedButton =
|
||||||
|
await budgetPage.getButtonForBudgeted(categoryName);
|
||||||
|
|
||||||
|
await expect(budgetedButton).toHaveText('100.00');
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spent Cell Tests
|
||||||
|
|
||||||
|
test('checks that clicking spent cell redirects to the category transactions page', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryName = await budgetPage.getCategoryNameForRow(0);
|
||||||
|
const accountPage = await budgetPage.openSpentPage(categoryName);
|
||||||
|
|
||||||
|
await expect(accountPage.heading).toContainText(categoryName);
|
||||||
|
await expect(accountPage.transactionList).toBeVisible();
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Balance Cell Tests
|
||||||
|
|
||||||
|
test('checks that clicking the balance cell opens the balance menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
const categoryName = await budgetPage.getCategoryNameForRow(0);
|
||||||
|
await budgetPage.openBalanceMenu(categoryName);
|
||||||
|
|
||||||
|
const balanceMenuModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(balanceMenuModalHeading).toHaveText(categoryName);
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (budgetType === 'Envelope') {
|
||||||
|
test('checks that clicking the To Budget/Overbudgeted amount opens the budget summary menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
await budgetPage.openEnvelopeBudgetSummaryMenu();
|
||||||
|
|
||||||
|
const summaryModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(summaryModalHeading).toHaveText('Budget Summary');
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budgetType === 'Tracking') {
|
||||||
|
test('checks that clicking the Saved/Projected Savings/Overspent amount opens the budget summary menu modal', async () => {
|
||||||
|
const budgetPage = await navigation.goToBudgetPage();
|
||||||
|
await budgetPage.waitForBudgetTable();
|
||||||
|
|
||||||
|
await budgetPage.openTrackingBudgetSummaryMenu();
|
||||||
|
|
||||||
|
const summaryModalHeading = page
|
||||||
|
.getByRole('dialog')
|
||||||
|
.getByRole('heading');
|
||||||
|
|
||||||
|
await expect(summaryModalHeading).toHaveText('Budget Summary');
|
||||||
|
await expect(page).toMatchThemeScreenshots();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 29 KiB |