From e1d69e236fba90279f2931502eeaecbae8d23703 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 21 Jan 2022 16:43:56 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20e2e=E3=83=86=E3=82=B9=E3=83=88?=
 =?UTF-8?q?=E3=82=92=E3=81=A7=E3=81=8D=E3=82=8B=E3=81=A0=E3=81=91=E6=94=B9?=
 =?UTF-8?q?=E8=89=AF=E3=81=97=E3=81=A6=E3=81=BF=E3=81=9F=20(#8159)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* update docker image?

* 続

* serial run delete from "${table}" cascade

* use cypress official github action

* refuse install by cypress action

* clean up

* use wait?

* use more wait?

* Revert "use more wait?"

This reverts commit 18d0fcae9c7d8f98a4cafb4a846a031ece57350c.

* Revert "use wait?"

This reverts commit 5aa8feec9cdc3e2f79e566249f0a0eff6c0df6a0.

* fix

* test

* test

* log?

* 握りつぶしてみる

* clean up

* env?

* clean up?

* disable video

* add comment

* remove test

* 成功?

* test browser

* nodeインストール無効化

* node16.13.0-chrome95-ff94

* node.js復活

* ?

* ちょっと戻してみる

* chrome?

* cross browser test2

* --shm-size=2g

* artifact?

* misskey.local?

* firefoxはあきらめる

* not headless?

* oops

* fix

* ??

* test1

* if?

* fail-fast: false

* headless: false

* easy error ignoreing describe

* エラーの解消
とちょっとリファクター

* add browser name to artifact

* Install mplayer for FireFox

* no wait?

* タイムアウトを甘くしてみる

* firefoxをあきらめる(n回目)

* remove timeout setting

* wait復活

* Update basic.js

* Update index.js

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 .github/workflows/test.yml                    | 39 ++++++++++++---
 cypress/integration/basic.js                  | 44 +++++++----------
 cypress/support/index.js                      | 12 +++--
 packages/backend/src/db/postgre.ts            |  4 +-
 packages/backend/test/docker-compose.yml      |  4 +-
 packages/client/src/components/notes.vue      |  4 +-
 packages/client/src/components/timeline.vue   | 12 ++---
 .../client/src/components/ui/pagination.vue   | 47 +++++++++++--------
 packages/client/src/pages/tag.vue             |  2 +-
 .../client/src/pages/user/index.timeline.vue  |  2 +-
 10 files changed, 99 insertions(+), 71 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6a684eaa48..3e9585f96d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,14 +17,14 @@ jobs:
 
     services:
       postgres:
-        image: postgres:12.2-alpine
+        image: postgres:13
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
       redis:
-        image: redis:4.0-alpine
+        image: redis:6
         ports:
           - 56312:6379
 
@@ -51,19 +51,21 @@ jobs:
     runs-on: ubuntu-latest
 
     strategy:
+      fail-fast: false
       matrix:
         node-version: [16.x]
+        browser: [chrome]
 
     services:
       postgres:
-        image: postgres:12.2-alpine
+        image: postgres:13
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
       redis:
-        image: redis:4.0-alpine
+        image: redis:6
         ports:
           - 56312:6379
 
@@ -71,6 +73,12 @@ jobs:
     - uses: actions/checkout@v2
       with:
         submodules: true
+    # https://github.com/cypress-io/cypress-docker-images/issues/150
+    #- name: Install mplayer for FireFox
+    #  run: sudo apt install mplayer -y
+    #  if: ${{ matrix.browser == 'firefox' }}
+    #- uses: browser-actions/setup-firefox@latest
+    #  if: ${{ matrix.browser == 'firefox' }}
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
@@ -87,5 +95,24 @@ jobs:
       run: cp .github/misskey/test.yml .config
     - name: Build
       run: yarn build
-    - name: Test
-      run: yarn e2e
+    # https://github.com/cypress-io/cypress/issues/4351#issuecomment-559489091
+    - name: ALSA Env
+      run: echo -e 'pcm.!default {\n type hw\n card 0\n}\n\nctl.!default {\n type hw\n card 0\n}' > ~/.asoundrc
+    - name: Cypress run
+      uses: cypress-io/github-action@v2
+      with:
+        install: false
+        start: npm run start:test
+        wait-on: 'http://localhost:61812'
+        headless: false
+        browser: ${{ matrix.browser }}
+    - uses: actions/upload-artifact@v2
+      if: failure()
+      with:
+        name: ${{ matrix.browser }}-cypress-screenshots
+        path: cypress/screenshots
+    - uses: actions/upload-artifact@v2
+      if: always()
+      with:
+        name: ${{ matrix.browser }}-cypress-videos
+        path: cypress/videos
diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js
index a754f41b98..aca44ef15d 100644
--- a/cypress/integration/basic.js
+++ b/cypress/integration/basic.js
@@ -41,8 +41,6 @@ describe('After setup instance', () => {
 			username: 'admin',
 			password: 'pass',
 		}).its('body').as('admin');
-
-		cy.get('@admin');
 	});
 
 	afterEach(() => {
@@ -82,15 +80,11 @@ describe('After user signup', () => {
 			password: 'pass',
 		}).its('body').as('admin');
 
-		cy.get('@admin').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).its('body').as('alice');
-		});
-
-		cy.get('@alice');
+		// ユーザー作成
+		cy.request('POST', '/api/signup', {
+			username: 'alice',
+			password: 'alice1234',
+		}).its('body').as('alice');
 	});
 
 	afterEach(() => {
@@ -145,27 +139,21 @@ describe('After user singed in', () => {
 			password: 'pass',
 		}).its('body').as('admin');
 
-		cy.get('@admin').then(() => {
-			// ユーザー作成
-			cy.request('POST', '/api/signup', {
-				username: 'alice',
-				password: 'alice1234',
-			}).its('body').as('alice');
-		});
+		// ユーザー作成
+		cy.request('POST', '/api/signup', {
+			username: 'alice',
+			password: 'alice1234',
+		}).its('body').as('alice');
 
-		cy.get('@alice').then(() => {
-			cy.visit('/');
+		cy.visit('/');
 
-			cy.intercept('POST', '/api/signin').as('signin');
+		cy.intercept('POST', '/api/signin').as('signin');
 
-			cy.get('[data-cy-signin]').click();
-			cy.get('[data-cy-signin-username] input').type('alice');
-			cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+		cy.get('[data-cy-signin]').click();
+		cy.get('[data-cy-signin-username] input').type('alice');
+		cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
 
-			cy.wait('@signin').as('signedIn');
-		});
-
-		cy.get('@signedIn');
+		cy.wait('@signin').as('signedIn');
 	});
 
 	afterEach(() => {
diff --git a/cypress/support/index.js b/cypress/support/index.js
index a9ac34476d..9185be344c 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -20,7 +20,13 @@ import './commands'
 // require('./commands')
 
 Cypress.on('uncaught:exception', (err, runnable) => {
-  if (err.message.includes('ResizeObserver loop limit exceeded')) {
-    return false
-  }
+	if ([
+		// Chrome
+		'ResizeObserver loop limit exceeded',
+
+		// Firefox
+		'ResizeObserver loop completed with undelivered notifications',
+	].some(msg => err.message.includes(msg))) {
+		return false;
+	}
 });
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index 1692b26219..69336c2a46 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -220,7 +220,9 @@ export async function resetDb() {
 		WHERE nspname NOT IN ('pg_catalog', 'information_schema')
 			AND C.relkind = 'r'
 			AND nspname !~ '^pg_toast';`);
-		await Promise.all(tables.map(t => t.table).map(x => conn.query(`DELETE FROM "${x}" CASCADE`)));
+		for (const table of tables) {
+			await conn.query(`DELETE FROM "${table.table}" CASCADE`);
+		}
 	};
 
 	for (let i = 1; i <= 3; i++) {
diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml
index c045e7c6c4..5f95bec4c0 100644
--- a/packages/backend/test/docker-compose.yml
+++ b/packages/backend/test/docker-compose.yml
@@ -2,12 +2,12 @@ version: "3"
 
 services:
   redistest:
-    image: redis:4.0-alpine
+    image: redis:6
     ports:
       - "127.0.0.1:56312:6379"
 
   dbtest:
-    image: postgres:12.2-alpine
+    image: postgres:13
     ports:
       - "127.0.0.1:54312:5432"
     environment:
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
index aec478ac95..41bec5a579 100644
--- a/packages/client/src/components/notes.vue
+++ b/packages/client/src/components/notes.vue
@@ -32,9 +32,7 @@ const props = defineProps<{
 const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 
 defineExpose({
-	prepend: (note) => {
-		pagingComponent.value?.prepend(note);
-	},
+	pagingComponent,
 });
 </script>
 
diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue
index a7af02c30b..59956b9526 100644
--- a/packages/client/src/components/timeline.vue
+++ b/packages/client/src/components/timeline.vue
@@ -25,10 +25,10 @@ const emit = defineEmits<{
 
 provide('inChannel', computed(() => props.src === 'channel'));
 
-const tlComponent = ref<InstanceType<typeof XNotes>>();
+const tlComponent: InstanceType<typeof XNotes> = $ref();
 
 const prepend = note => {
-	tlComponent.value.prepend(note);
+	tlComponent.pagingComponent?.prepend(note);
 
 	emit('note');
 
@@ -38,16 +38,16 @@ const prepend = note => {
 };
 
 const onUserAdded = () => {
-	tlComponent.value.reload();
+	tlComponent.pagingComponent?.reload();
 };
 
 const onUserRemoved = () => {
-	tlComponent.value.reload();
+	tlComponent.pagingComponent?.reload();
 };
 
 const onChangeFollowing = () => {
-	if (!tlComponent.value.backed) {
-		tlComponent.value.reload();
+	if (!tlComponent.pagingComponent?.backed) {
+		tlComponent.pagingComponent?.reload();
 	}
 };
 
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 571ef71eab..9c18fc5ce5 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -73,12 +73,11 @@ const queue = ref<Item[]>([]);
 const offset = ref(0);
 const fetching = ref(true);
 const moreFetching = ref(false);
-const inited = ref(false);
 const more = ref(false);
 const backed = ref(false); // 遡り中か否か
 const isBackTop = ref(false);
-const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value);
-const error = computed(() => !fetching.value && !inited.value);
+const empty = computed(() => items.value.length === 0);
+const error = ref(false);
 
 const init = async (): Promise<void> => {
 	queue.value = [];
@@ -105,9 +104,10 @@ const init = async (): Promise<void> => {
 			more.value = false;
 		}
 		offset.value = res.length;
-		inited.value = true;
+		error.value = false;
 		fetching.value = false;
 	}, e => {
+		error.value = true;
 		fetching.value = false;
 	});
 };
@@ -183,30 +183,36 @@ const fetchMoreAhead = async (): Promise<void> => {
 };
 
 const prepend = (item: Item): void => {
-	if (rootEl.value == null) return;
-
 	if (props.pagination.reversed) {
-		const container = getScrollContainer(rootEl.value);
-		if (container == null) return; // TODO?
+		if (rootEl.value) {
+			const container = getScrollContainer(rootEl.value);
+			if (container == null) return; // TODO?
 
-		const pos = getScrollPosition(rootEl.value);
-		const viewHeight = container.clientHeight;
-		const height = container.scrollHeight;
-		const isBottom = (pos + viewHeight > height - 32);
-		if (isBottom) {
-			// オーバーフローしたら古いアイテムは捨てる
-			if (items.value.length >= props.displayLimit) {
-				// このやり方だとVue 3.2以降アニメーションが動かなくなる
-				//items.value = items.value.slice(-props.displayLimit);
-				while (items.value.length >= props.displayLimit) {
-					items.value.shift();
+			const pos = getScrollPosition(rootEl.value);
+			const viewHeight = container.clientHeight;
+			const height = container.scrollHeight;
+			const isBottom = (pos + viewHeight > height - 32);
+			if (isBottom) {
+				// オーバーフローしたら古いアイテムは捨てる
+				if (items.value.length >= props.displayLimit) {
+					// このやり方だとVue 3.2以降アニメーションが動かなくなる
+					//items.value = items.value.slice(-props.displayLimit);
+					while (items.value.length >= props.displayLimit) {
+						items.value.shift();
+					}
+					more.value = true;
 				}
-				more.value = true;
 			}
 		}
 		items.value.push(item);
 		// TODO
 	} else {
+		// 初回表示時はunshiftだけでOK
+		if (!rootEl.value) {
+			items.value.unshift(item);
+			return;
+		}
+
 		const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
 
 		if (isTop) {
@@ -264,6 +270,7 @@ onDeactivated(() => {
 
 defineExpose({
 	items,
+	backed,
 	reload,
 	fetchMoreAhead,
 	prepend,
diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue
index 8d8dc0a65c..045f1ef259 100644
--- a/packages/client/src/pages/tag.vue
+++ b/packages/client/src/pages/tag.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="_section">
-	<XNotes ref="notes" class="_content" :pagination="pagination"/>
+	<XNotes class="_content" :pagination="pagination"/>
 </div>
 </template>
 
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
index 7396a76efe..a1329a7411 100644
--- a/packages/client/src/pages/user/index.timeline.vue
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -5,7 +5,7 @@
 		<option value="replies">{{ $ts.notesAndReplies }}</option>
 		<option value="files">{{ $ts.withFiles }}</option>
 	</MkTab>
-	<XNotes ref="timeline" :no-gap="true" :pagination="pagination"/>
+	<XNotes :no-gap="true" :pagination="pagination"/>
 </div>
 </template>