-
Notifications
You must be signed in to change notification settings - Fork 588
Warn user when note sync fails #3387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Usiel
wants to merge
10
commits into
trunk
Choose a base branch
from
usielriedl/add-warning-for-large-notes
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
e3a5cba
Warn in the note list when sync fails and allow retries.
Usiel 92c4018
Make use of accessibility primitives for UI tests
Usiel 82038d6
Track note syncing for pending status icons
Usiel ead0fa2
Simplify and fix stuck queue handling
Usiel ac4173e
Hedge risky bucket operations behind 413 errors
Usiel 4262f87
Refactor: Rename state clearing actions
Usiel 03ce3ed
Persist sync errors
Usiel d8e0acd
Single indicator for sync
Usiel 5b55d64
Remove 413 queue unstuck code
Usiel 0190fb7
Test each state we care about
Usiel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import React from 'react'; | ||
| import { render } from '@testing-library/react'; | ||
|
|
||
| import { NoteCell } from './note-cell'; | ||
| import { getSyncErrorMessage } from '../utils/sync-error-message'; | ||
| import type * as T from '../types'; | ||
|
|
||
| const noteId = 'note-1' as T.EntityId; | ||
|
|
||
| const minimalNote: T.Note = { | ||
| content: 'Hello world', | ||
| creationDate: 0, | ||
| deleted: false, | ||
| modificationDate: 0, | ||
| systemTags: [], | ||
| tags: [], | ||
| }; | ||
|
|
||
| const baseProps = { | ||
| displayMode: 'comfy' as T.ListDisplayMode, | ||
| invalidateHeight: jest.fn(), | ||
| isOffline: false, | ||
| isOpened: false, | ||
| isSyncing: false, | ||
| lastUpdated: -Infinity, | ||
| note: minimalNote, | ||
| noteId, | ||
| openNote: jest.fn(), | ||
| pinNote: jest.fn(), | ||
| searchQuery: '', | ||
| hasPendingChanges: false, | ||
| style: {}, | ||
| syncErrorCode: null as number | null, | ||
| }; | ||
|
|
||
| const expectSpinning = (icon: HTMLElement, spinning: boolean) => { | ||
| expect(icon.classList.contains('is-syncing')).toBe(spinning); | ||
| }; | ||
|
|
||
| const expectOfflineStyle = (icon: HTMLElement, offline: boolean) => { | ||
| expect(icon.classList.contains('is-offline')).toBe(offline); | ||
| }; | ||
|
|
||
| const expectErrorStyle = (icon: HTMLElement, errored: boolean) => { | ||
| expect(icon.classList.contains('has-sync-error')).toBe(errored); | ||
| }; | ||
|
|
||
| describe('NoteCell status icons', () => { | ||
| describe('pending sync icon appearance', () => { | ||
| it('spinning spinner shows when pending changes regardless of syncing state', () => { | ||
| const { getByRole } = render( | ||
| <NoteCell {...baseProps} hasPendingChanges /> | ||
| ); | ||
| const icon = getByRole('img', { name: 'Pending changes' }); | ||
|
|
||
| expectSpinning(icon, true); | ||
| expectOfflineStyle(icon, false); | ||
| expectErrorStyle(icon, false); | ||
| }); | ||
|
|
||
| it('non-spinning spinner shows when pending changes and offline', () => { | ||
| const { getByRole } = render( | ||
| <NoteCell {...baseProps} hasPendingChanges isOffline /> | ||
| ); | ||
| const icon = getByRole('img', { | ||
| name: 'Pending changes (waiting for network connection)', | ||
| }); | ||
|
|
||
| expectSpinning(icon, false); | ||
| expectOfflineStyle(icon, true); | ||
| expectErrorStyle(icon, false); | ||
| }); | ||
|
|
||
| it('non-spinning spinner shows when we have a sync error and we are not actively syncing', () => { | ||
| const { getByRole, queryByRole } = render( | ||
| <NoteCell {...baseProps} hasPendingChanges syncErrorCode={413} /> | ||
| ); | ||
| const icon = getByRole('img', { | ||
| name: 'Sync failed', | ||
| description: getSyncErrorMessage(413), | ||
| }); | ||
|
|
||
| expectSpinning(icon, false); | ||
| expectErrorStyle(icon, true); | ||
| expect(queryByRole('img', { name: 'Pending changes' })).toBeNull(); | ||
| }); | ||
|
|
||
| it('spinning spinner shows when we are actively syncing and have a sync error', () => { | ||
| const { getByRole, queryByRole } = render( | ||
| <NoteCell | ||
| {...baseProps} | ||
| hasPendingChanges | ||
| isSyncing | ||
| syncErrorCode={413} | ||
| /> | ||
| ); | ||
| const icon = getByRole('img', { name: 'Pending changes' }); | ||
|
|
||
| expectSpinning(icon, true); | ||
| expectErrorStyle(icon, false); | ||
| expect(queryByRole('img', { name: 'Sync failed' })).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when the note is not pending', () => { | ||
| it('does not render a sync icon', () => { | ||
| const { queryByRole } = render(<NoteCell {...baseProps} />); | ||
|
|
||
| expect(queryByRole('img', { name: 'Sync failed' })).toBeNull(); | ||
| expect(queryByRole('img', { name: 'Pending changes' })).toBeNull(); | ||
| }); | ||
|
|
||
| it('still renders a sync error icon when syncErrorCode is set', () => { | ||
| const { getByRole } = render( | ||
| <NoteCell {...baseProps} syncErrorCode={413} /> | ||
| ); | ||
|
|
||
| expect( | ||
| getByRole('img', { | ||
| name: 'Sync failed', | ||
| description: getSyncErrorMessage(413), | ||
| }) | ||
| ).not.toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('published', () => { | ||
| it('does not render a published icon when the note is not published', () => { | ||
| const { queryByRole } = render(<NoteCell {...baseProps} />); | ||
|
|
||
| expect(queryByRole('img', { name: 'Published note' })).toBeNull(); | ||
| }); | ||
|
|
||
| it('renders a published icon when publishURL is set', () => { | ||
| const { getByRole } = render( | ||
| <NoteCell | ||
| {...baseProps} | ||
| note={{ | ||
| ...minimalNote, | ||
| publishURL: 'https://publish.simplenote.com/abc', | ||
| }} | ||
| /> | ||
| ); | ||
|
|
||
| expect(getByRole('img', { name: 'Published note' })).not.toBeNull(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import type { Ghost } from 'simperium'; | ||
|
|
||
| import { noteHasPendingChanges } from './selectors'; | ||
| import type * as S from './'; | ||
| import type * as T from '../types'; | ||
|
|
||
| const noteId = 'note-1' as T.EntityId; | ||
|
|
||
| const localNote: T.Note = { | ||
| content: 'local content', | ||
| creationDate: 0, | ||
| deleted: false, | ||
| modificationDate: 1, | ||
| systemTags: [], | ||
| tags: [], | ||
| }; | ||
|
|
||
| const ghostNote: T.Note = { | ||
| ...localNote, | ||
| content: 'server content', | ||
| }; | ||
|
|
||
| const makeState = ( | ||
| overrides: { | ||
| note?: T.Note; | ||
| ghost?: T.Note; | ||
| } = {} | ||
| ): S.State => { | ||
| const note = overrides.note ?? localNote; | ||
| const ghost = overrides.ghost ?? ghostNote; | ||
| const noteGhosts = new Map<T.EntityId, Ghost<T.Note>>(); | ||
|
|
||
| if (ghost) { | ||
| noteGhosts.set(noteId, { data: ghost } as Ghost<T.Note>); | ||
| } | ||
|
|
||
| return { | ||
| data: { | ||
| notes: new Map([[noteId, note]]), | ||
| }, | ||
| simperium: { | ||
| ghosts: [new Map(), new Map([['note', noteGhosts]])], | ||
| }, | ||
| } as unknown as S.State; | ||
| }; | ||
|
|
||
| describe('noteHasPendingChanges', () => { | ||
| it('returns false when local and ghost notes match', () => { | ||
| const state = makeState({ ghost: localNote }); | ||
|
|
||
| expect(noteHasPendingChanges(state, noteId)).toBe(false); | ||
| }); | ||
|
|
||
| it('returns true when local and ghost notes differ', () => { | ||
| const state = makeState(); | ||
|
|
||
| expect(noteHasPendingChanges(state, noteId)).toBe(true); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on a similar note, it would be semantically communicative if this didn’t indicate a stateful UI operation, but rather communicated declarative semantics about the state of the note.
trying to stew on names that differentiate between expected sync failures (such as network failure or being offline) and affirmatively rejected syncs that won’t succeed on retry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now, this state really just represents errors we receive from the server, so to me
syncErrorCodeis pretty clear, as in "this is the error code we got while syncing for this note".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it probably seems pretty clear at a glance, but what if the note contents have changed since we received the sync error code, or if we received a remote update to the note? the sync error code was for a version of the note that no longer exists and the error code is out of date, unless we expect it to flash and disappear in these cases.
if we do expect it to automatically flash and disappear, that feels different than a pending-changes indicator because we kind of expected changes to enter a transient state of pending, but it would give me anxiety if I saw an error message appear and then disappear before I get a chance to read it.