Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8b69268d | |||
| 5eb5dbb533 | |||
| 1233e9e193 | |||
| 8cbe68485d |
+86
@@ -0,0 +1,86 @@
|
||||
/** @type {Detox.DetoxConfig} */
|
||||
module.exports = {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: 'jest',
|
||||
config: '__e2e__/jest.config.js',
|
||||
},
|
||||
jest: {
|
||||
setupTimeout: 120000,
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
'ios.debug': {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
|
||||
build:
|
||||
'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
|
||||
},
|
||||
'ios.release': {
|
||||
type: 'ios.app',
|
||||
binaryPath:
|
||||
'ios/build/Build/Products/Release-iphonesimulator/bluesky.app',
|
||||
build:
|
||||
'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
|
||||
},
|
||||
'android.debug': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
||||
build:
|
||||
'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -',
|
||||
reversePorts: [8081],
|
||||
},
|
||||
'android.release': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
|
||||
build:
|
||||
'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -',
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
simulator: {
|
||||
type: 'ios.simulator',
|
||||
device: {
|
||||
type: 'iPhone 15 Pro',
|
||||
},
|
||||
},
|
||||
attached: {
|
||||
type: 'android.attached',
|
||||
device: {
|
||||
adbName: '.*',
|
||||
},
|
||||
},
|
||||
emulator: {
|
||||
type: 'android.emulator',
|
||||
device: {
|
||||
avdName: 'Pixel_3a_API_30_x86',
|
||||
},
|
||||
},
|
||||
},
|
||||
configurations: {
|
||||
'ios.sim.debug': {
|
||||
device: 'simulator',
|
||||
app: 'ios.debug',
|
||||
},
|
||||
'ios.sim.release': {
|
||||
device: 'simulator',
|
||||
app: 'ios.release',
|
||||
},
|
||||
'android.att.debug': {
|
||||
device: 'attached',
|
||||
app: 'android.debug',
|
||||
},
|
||||
'android.att.release': {
|
||||
device: 'attached',
|
||||
app: 'android.release',
|
||||
},
|
||||
'android.emu.debug': {
|
||||
device: 'emulator',
|
||||
app: 'android.debug',
|
||||
},
|
||||
'android.emu.release': {
|
||||
device: 'emulator',
|
||||
app: 'android.release',
|
||||
},
|
||||
},
|
||||
}
|
||||
+2
-1
@@ -95,7 +95,8 @@ web-build/
|
||||
/ios/
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Firebase (Android) Google services
|
||||
# INCLUDED: google-services.json
|
||||
# INCLUDED: google-services.json
|
||||
@@ -1,2 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
||||
@@ -4,5 +4,3 @@ SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
|
||||
+1
-79
@@ -7,84 +7,10 @@ module.exports = {
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'lingui',
|
||||
'simple-import-sort',
|
||||
'bsky-internal',
|
||||
'eslint-plugin-react-compiler',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'detox', 'react', 'lingui'],
|
||||
rules: {
|
||||
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
|
||||
'prettier/prettier': 0,
|
||||
'react/no-unescaped-entities': 0,
|
||||
'react-native/no-inline-styles': 0,
|
||||
'bsky-internal/avoid-unwrapped-text': [
|
||||
'error',
|
||||
{
|
||||
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
Button: 'ButtonText',
|
||||
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||
},
|
||||
},
|
||||
],
|
||||
'bsky-internal/use-exact-imports': 'error',
|
||||
'bsky-internal/use-typed-gates': 'error',
|
||||
'bsky-internal/use-prefixed-imports': 'warn',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Node.js builtins prefixed with `node:`.
|
||||
['^node:'],
|
||||
// Packages.
|
||||
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
|
||||
// React/React Native priortized, followed by expo
|
||||
// Followed by all packages excluding unprefixed relative ones
|
||||
[
|
||||
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
|
||||
'^(expo(.*)$)|^(expo$)',
|
||||
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
|
||||
],
|
||||
// Relative imports.
|
||||
// Ideally, anything that starts with a dot or #
|
||||
// due to unprefixed relative imports being used, we whitelist the relative paths we use
|
||||
// (?:$|\\/) matches end of string or /
|
||||
[
|
||||
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
|
||||
'^(?:#\\/)?view(?:$|\\/)',
|
||||
'^(?:#\\/)?screens(?:$|\\/)',
|
||||
'^(?:#\\/)?alf(?:$|\\/)',
|
||||
'^(?:#\\/)?components(?:$|\\/)',
|
||||
'^#\\/',
|
||||
'^\\.',
|
||||
],
|
||||
// anything else - hopefully we don't have any of these
|
||||
['^'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'warn',
|
||||
// TODO: Reenable when we figure out why it gets stuck on CI.
|
||||
// 'react-compiler/react-compiler': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@atproto/api',
|
||||
importNames: ['moderatePost'],
|
||||
message:
|
||||
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
@@ -105,8 +31,4 @@ module.exports = {
|
||||
settings: {
|
||||
componentWrapperFunctions: ['observer'],
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
name: build-and-push-bskyweb-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- traffic-reduction
|
||||
- respect-optout-for-embeds
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
@@ -43,10 +43,6 @@ jobs:
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Set outputs
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
@@ -58,5 +54,3 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
name: build-and-push-embedr-aws
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- bnewbold/embedr
|
||||
- bnewbold/embedr-rebase
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: embed
|
||||
|
||||
jobs:
|
||||
embedr-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.embedr
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,53 +0,0 @@
|
||||
name: build-and-push-link-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: bskylink
|
||||
|
||||
jobs:
|
||||
link-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.bskylink
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,53 +0,0 @@
|
||||
name: build-and-push-ogcard-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: bskyogcard
|
||||
|
||||
jobs:
|
||||
ogcard-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.bskyogcard
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
type: choice
|
||||
description: Build profile to use
|
||||
options:
|
||||
- testflight-android
|
||||
- production
|
||||
|
||||
jobs:
|
||||
@@ -25,8 +24,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -34,9 +31,6 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
@@ -62,105 +56,10 @@ jobs:
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
|
||||
run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive
|
||||
|
||||
- name: ✍️ Rename Testflight bundle
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
run: mv build.aab build.apk
|
||||
|
||||
- name: ⏰ Get a timestamp
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
|
||||
- name: 🚀 Upload Production Artifact
|
||||
id: upload-artifact-production
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.aab
|
||||
path: build.aab
|
||||
|
||||
- name: 🚀 Upload Testflight Artifact
|
||||
id: upload-artifact-testflight
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🔔 Notify Slack of Testflight Build
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
|
||||
|
||||
- name: 🚀 Upload Production APK Artifact
|
||||
id: upload-artifact-production-apk
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack of Production APK Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p android --non-interactive --path build.aab
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
name: Build and Submit iOS
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
type: choice
|
||||
description: Build profile to use
|
||||
options:
|
||||
- testflight
|
||||
- production
|
||||
|
||||
jobs:
|
||||
@@ -25,8 +26,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -34,9 +33,6 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
@@ -50,10 +46,6 @@ jobs:
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
@@ -74,40 +66,10 @@ jobs:
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
|
||||
run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
---
|
||||
name: Bundle and Deploy EAS Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
type: choice
|
||||
description: Deployment channel to use
|
||||
options:
|
||||
- testflight
|
||||
- production
|
||||
runtimeVersion:
|
||||
type: string
|
||||
description: Runtime version (in x.x.x format) that this update is for
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bundleDeploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
|
||||
cancel-in-progress: true
|
||||
outputs:
|
||||
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate the version if one is supplied. This should generally happen if the update is for a production client
|
||||
- name: 🧐 Validate version
|
||||
if: ${{ inputs.runtimeVersion }}
|
||||
run: |
|
||||
if [ -z "${{ inputs.runtimeVersion }}" ]; then
|
||||
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
run: git fetch origin main:main --depth 100
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
with:
|
||||
profile: ${{ inputs.channel || 'testflight' }}
|
||||
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
id: pods-cache
|
||||
with:
|
||||
path: ./ios/Pods
|
||||
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
|
||||
# change unless the yarn version changes as well.
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
buildIfNecessaryAndroid:
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
|
||||
cancel-in-progress: false
|
||||
needs: [ bundleDeploy ]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
|
||||
|
||||
- name: ⏰ Get a timestamp
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
|
||||
- name: 🚀 Upload Artifact
|
||||
id: upload-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
go-version: '1.21'
|
||||
- name: Dummy JS File
|
||||
run: touch bskyweb/static/js/blah.js
|
||||
- name: Check
|
||||
run: cd bskyweb/ && make check
|
||||
- name: Build (binary)
|
||||
@@ -36,8 +36,8 @@ jobs:
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
go-version: '1.21'
|
||||
- name: Dummy JS File
|
||||
run: touch bskyweb/static/js/blah.js
|
||||
- name: Lint
|
||||
run: cd bskyweb/ && make lint
|
||||
|
||||
@@ -24,8 +24,6 @@ jobs:
|
||||
attempt_delay: 2000
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
- name: Type check
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Credit for fingerprint action https://github.com/expo/expo
|
||||
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
|
||||
---
|
||||
name: PR Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
webpack-analyzer:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: Ensure tracking relevant branches and checkout base
|
||||
run: |
|
||||
git checkout ${{ github.head_ref }}
|
||||
git checkout ${{ github.base_ref }}
|
||||
|
||||
- name: Get the base commit
|
||||
id: base-commit
|
||||
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Merge PR commit
|
||||
run: |
|
||||
# Have to set a git config for the merge to work
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git merge --no-edit ${{ github.head_ref }}
|
||||
yarn install
|
||||
|
||||
- name: 🔦 Generate stats file for PR
|
||||
run: |
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json ../stats-new.json
|
||||
|
||||
- name: ⬇️ Get base stats from cache
|
||||
id: get-base-stats
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: stats-base.json
|
||||
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
|
||||
|
||||
- name: Restore to base commit
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
git reset HEAD~
|
||||
git restore .
|
||||
|
||||
- name: 🔦 Generate stats file from base commit
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
yarn install
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json stats-base.json
|
||||
|
||||
- name: % Get diff
|
||||
id: get-diff
|
||||
uses: NejcZdovc/bundle-size-diff@v1
|
||||
with:
|
||||
base_path: 'stats-base.json'
|
||||
pr_path: '../stats-new.json'
|
||||
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: bundle-diff
|
||||
message: |
|
||||
| Old size | New size | Diff |
|
||||
|----------|----------|-----------------------|
|
||||
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
|
||||
---
|
||||
|
||||
fingerprint-native:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
run: git fetch origin main:main --depth 100
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
with:
|
||||
profile: pull-request
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
message: |
|
||||
The Pull Request introduced fingerprint changes against the base commit:
|
||||
<details><summary>Fingerprint diff</summary>
|
||||
|
||||
```json
|
||||
${{ steps.fingerprint.outputs.diff }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- name: 💬 Delete comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
|
||||
+1
-12
@@ -18,6 +18,7 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -77,7 +78,6 @@ dist/
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
stats.json
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
@@ -104,17 +104,6 @@ google-services.json
|
||||
# Performance results (Flashlight)
|
||||
.perf/
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# i18n
|
||||
src/locale/locales/_build/
|
||||
src/locale/locales/**/*.js
|
||||
|
||||
# local builds
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
# ogcard assets
|
||||
bskyogcard/src/assets/fonts/noto-*
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
yarn lint-staged
|
||||
|
||||
+11
-16
@@ -1,17 +1,12 @@
|
||||
# Ignore everything except JS-ey or CSS code.
|
||||
# Based on https://stackoverflow.com/a/70715829/458193
|
||||
*
|
||||
!**/*.js
|
||||
!**/*.jsx
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
!*/
|
||||
|
||||
!**/*.css
|
||||
|
||||
# More specific ignores go below.
|
||||
.expo
|
||||
android
|
||||
ios
|
||||
src/locale/locales
|
||||
lib/react-compiler-runtime
|
||||
android
|
||||
src/third-party
|
||||
src/app.json
|
||||
public
|
||||
/bskyweb/templates
|
||||
/dist/
|
||||
/.watchmanconfig
|
||||
/app.json
|
||||
|
||||
web/index.html
|
||||
web-build/*
|
||||
|
||||
+3
-12
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
FROM golang:1.21-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
@@ -15,10 +15,6 @@ ENV GOARCH="amd64"
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT="loopvar"
|
||||
|
||||
# Expo
|
||||
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
|
||||
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER ${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
|
||||
|
||||
COPY . .
|
||||
|
||||
#
|
||||
@@ -33,13 +29,10 @@ RUN mkdir --parents $NVM_DIR && \
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
yarn intl:build && \
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() yarn build-web
|
||||
yarn intl:compile && \
|
||||
yarn build-web
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/static && find ./web-build/static
|
||||
@@ -79,5 +72,3 @@ CMD ["/usr/bin/bskyweb"]
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="bsky.app Web App"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
|
||||
# NOOP
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM node:20.11-alpine3.18 as build
|
||||
|
||||
# Move files into the image and install
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./bskylink/package.json ./
|
||||
COPY ./bskylink/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY ./bskylink ./
|
||||
|
||||
# build then prune dev deps
|
||||
RUN yarn build
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
# Uses assets from build stage to reduce build size
|
||||
FROM node:20.11-alpine3.18
|
||||
|
||||
RUN apk add --update dumb-init
|
||||
|
||||
# Avoid zombie processes, handle signal forwarding
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app /app
|
||||
RUN mkdir /app/data && chown node /app/data
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 3000
|
||||
ENV LINK_PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
# potential perf issues w/ io_uring on this version of node
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
|
||||
USER node
|
||||
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="Bsky Link Service"
|
||||
LABEL org.opencontainers.image.licenses=UNLICENSED
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM node:20.11-alpine3.18 as build
|
||||
|
||||
# Move files into the image and install
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./bskyogcard/package.json ./
|
||||
COPY ./bskyogcard/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY ./bskyogcard ./
|
||||
|
||||
# build then prune dev deps
|
||||
RUN yarn install-fonts && yarn build
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
# Uses assets from build stage to reduce build size
|
||||
FROM node:20.11-alpine3.18
|
||||
|
||||
RUN apk add --update dumb-init
|
||||
|
||||
# Avoid zombie processes, handle signal forwarding
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app /app
|
||||
RUN mkdir /app/data && chown node /app/data
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 3000
|
||||
ENV CARD_PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
# potential perf issues w/ io_uring on this version of node
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
|
||||
USER node
|
||||
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="Bsky Card Service"
|
||||
LABEL org.opencontainers.image.licenses=UNLICENSED
|
||||
@@ -1,80 +0,0 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Node
|
||||
ENV NODE_VERSION=18
|
||||
ENV NVM_DIR=/usr/share/nvm
|
||||
|
||||
# Go
|
||||
ENV GODEBUG="netdns=go"
|
||||
ENV GOOS="linux"
|
||||
ENV GOARCH="amd64"
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT="loopvar"
|
||||
|
||||
COPY . .
|
||||
|
||||
#
|
||||
# Generate the JavaScript webpack. NOTE: this will change
|
||||
#
|
||||
RUN mkdir --parents $NVM_DIR && \
|
||||
wget \
|
||||
--output-document=/tmp/nvm-install.sh \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
|
||||
bash /tmp/nvm-install.sh
|
||||
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
cd bskyembed && yarn install --frozen-lockfile && cd .. && \
|
||||
yarn intl:build && \
|
||||
yarn build-embed
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bskyembed/dist
|
||||
|
||||
# hack around issue with empty directory and go:embed
|
||||
RUN touch bskyweb/static/js/empty.txt
|
||||
RUN touch bskyweb/static/css/empty.txt
|
||||
RUN touch bskyweb/static/media/empty.txt
|
||||
|
||||
#
|
||||
# Generate the embedr Go binary.
|
||||
#
|
||||
RUN cd bskyweb/ && \
|
||||
go mod download && \
|
||||
go mod verify
|
||||
|
||||
RUN cd bskyweb/ && \
|
||||
go build \
|
||||
-v \
|
||||
-trimpath \
|
||||
-tags timetzdata \
|
||||
-o /embedr \
|
||||
./cmd/embedr
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ENV GODEBUG=netdns=go
|
||||
ENV TZ=Etc/UTC
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install --yes \
|
||||
dumb-init \
|
||||
ca-certificates
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /embedr
|
||||
COPY --from=build-env /embedr /usr/bin/embedr
|
||||
|
||||
CMD ["/usr/bin/embedr"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="embed.bsky.app Web App"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
@@ -13,11 +13,6 @@ build-web: ## Compile web bundle, copy to bskyweb directory
|
||||
yarn intl:build
|
||||
yarn build-web
|
||||
|
||||
.PHONY: build-web-embed
|
||||
build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directories
|
||||
yarn intl:build
|
||||
yarn build-embed
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run all tests
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
@@ -33,7 +28,6 @@ lint: ## Run style checks and verify syntax
|
||||
.PHONY: deps
|
||||
deps: ## Installs dependent libs using 'yarn install'
|
||||
yarn install --frozen-lockfile
|
||||
cd bskyembed && yarn install --frozen-lockfile
|
||||
|
||||
.PHONY: nvm-setup
|
||||
nvm-setup: ## Use NVM to install and activate node+yarn
|
||||
|
||||
@@ -10,7 +10,7 @@ Get the app itself:
|
||||
|
||||
## Development Resources
|
||||
|
||||
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
||||
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
||||
|
||||
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
|
||||
|
||||
@@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
|
||||
- Open an issue and give some time for discussion before submitting a PR.
|
||||
- Stay away from PRs like...
|
||||
- Changing "Post" to "Skeet."
|
||||
- Refactoring the codebase, e.g., to replace MobX with Redux or something.
|
||||
- Refactoring the codebase, eg to replace mobx with redux or something.
|
||||
- Adding entirely new features without prior discussion.
|
||||
|
||||
Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
||||
Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
||||
|
||||
## Forking guidelines
|
||||
|
||||
@@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app.
|
||||
|
||||
## Are you a developer interested in building on atproto?
|
||||
|
||||
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party integration can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||
|
||||
## License (MIT)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
flows:
|
||||
- "flows/*"
|
||||
@@ -1,30 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Post an image with the porn label
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "labelsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on porn"
|
||||
point: 78%,67%
|
||||
- tapOn:
|
||||
label: "Tap on confirm"
|
||||
point: 51%,92%
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- tapOn:
|
||||
id: "e2eRefreshHome"
|
||||
- assertVisible: "Adult Content"
|
||||
@@ -1,87 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "e2eRefreshHome"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
@@ -1,37 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ""
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn:
|
||||
id: "createAccountButton"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn:
|
||||
id: "customSelectBtn"
|
||||
- tapOn:
|
||||
id: "customServerTextInput"
|
||||
- inputText: "http://localhost:3000"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- tapOn:
|
||||
id: "emailInput"
|
||||
- inputText: "example@test.com"
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
id: "handleInput"
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&posts"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
label: "Create a curate list"
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
- tapOn:
|
||||
label: "Edit display name and description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- eraseText
|
||||
- inputText: "Bad Ppl"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: Bad Ppl
|
||||
- assertVisible: They bad
|
||||
|
||||
- tapOn:
|
||||
label: "Remove description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible:
|
||||
id: "listDescription"
|
||||
|
||||
- tapOn:
|
||||
label: "Delete the curatelist"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
- assertVisible: "This list is empty!"
|
||||
|
||||
- tapOn:
|
||||
label: "Create a new curatelist"
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
- tapOn: "About"
|
||||
|
||||
- tapOn:
|
||||
label: "Adds users on curatelists from the list"
|
||||
id: "addUserBtn"
|
||||
- assertVisible:
|
||||
id: "listAddUserModal"
|
||||
- tapOn:
|
||||
id: "searchInput"
|
||||
- inputText: "b"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "listAddUserModal"
|
||||
- assertVisible:
|
||||
id: "user-bob.test"
|
||||
|
||||
- tapOn: "Posts"
|
||||
- assertVisible:
|
||||
label: "Shows posts by the users in the list"
|
||||
id: "feedItem-by-bob.test"
|
||||
|
||||
- tapOn:
|
||||
label: "Pins the list"
|
||||
id: "pinBtn"
|
||||
- tapOn:
|
||||
id: "e2eGotoHome"
|
||||
- tapOn: "Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "e2eGotoFeeds"
|
||||
- tapOn:
|
||||
id: "saved-feed-Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "unpinBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible:
|
||||
id: "homeScreenFeedTabs-Good Ppl"
|
||||
- tapOn:
|
||||
id: "e2eGotoLists"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn: "About"
|
||||
- assertVisible:
|
||||
label: "Removes users on curatelists from the list"
|
||||
id: "user-bob.test"
|
||||
- tapOn:
|
||||
point: "90%,43%"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
|
||||
- tapOn:
|
||||
label: "Adds and removes users on curatelists from the profile"
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "bob"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Add to Lists"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Add to Lists"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
@@ -1,97 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Pin alice's feed
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "profileCardButton"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Set alice-favs first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
point: "79%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "Following"
|
||||
|
||||
# Set following first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
point: "79%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Remove following
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on unpin"
|
||||
point: "91%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertNotVisible: "Following"
|
||||
@@ -1,57 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "Feeds ✨"
|
||||
- tapOn:
|
||||
label: "Can go to feeds page using feeds button in tab bar"
|
||||
text: "Feeds ✨"
|
||||
- assertVisible: "Discover New Feeds"
|
||||
|
||||
- tapOn:
|
||||
label: "Feeds button disappears after pinning a feed"
|
||||
id: "bottomBarProfileBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
|
||||
- tapOn:
|
||||
label: "Can like posts"
|
||||
id: "likeBtn"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
|
||||
- tapOn:
|
||||
label: "Can repost posts"
|
||||
id: "repostBtn"
|
||||
- tapOn: "Repost"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn: "Remove repost"
|
||||
|
||||
- tapOn:
|
||||
label: "Can delete posts"
|
||||
id: "postDropdownBtn"
|
||||
childOf:
|
||||
id: "feedItem-by-alice.test"
|
||||
- tapOn: "Delete post"
|
||||
- tapOn: "Delete"
|
||||
@@ -1,26 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn: "Sign in"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn: "Custom"
|
||||
- tapOn:
|
||||
id: "customServerTextInput"
|
||||
- inputText: "http://localhost:3000"
|
||||
- tapOn: "Done"
|
||||
- tapOn:
|
||||
id: "loginUsernameInput"
|
||||
- inputText: "Alice"
|
||||
- tapOn:
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- assertVisible: "Following"
|
||||
@@ -1,45 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&labels"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# create a modlist
|
||||
- tapOn:
|
||||
id: "e2eGotoModeration"
|
||||
- tapOn:
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
# toggle mute subscription
|
||||
- tapOn:
|
||||
point: "70%,9%"
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
- tapOn: "Unmute"
|
||||
|
||||
# toggle block subscription
|
||||
- tapOn:
|
||||
point: "70%,9%"
|
||||
- tapOn: "Block accounts"
|
||||
- tapOn: "Block list"
|
||||
- tapOn: "Unblock"
|
||||
|
||||
# the rest of the behaviors are tested in curate-lists.yml
|
||||
@@ -1,32 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Open avatar creator"
|
||||
- tapOn: "Select the zap emoji as your avatar"
|
||||
- tapOn:
|
||||
label: "Tap on yellow"
|
||||
point: "23%,79%"
|
||||
- tapOn: "Done"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Select an avatar"
|
||||
- tapOn: "Select the atom emoji as your avatar"
|
||||
- tapOn: "Done"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Continue to next step"
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
label: "Tap on continue"
|
||||
point: "50%,92%"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
label: "Tap on Lets go"
|
||||
point: "50%,92%"
|
||||
@@ -1,28 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
point: "16%,22%"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Continue to next step"
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
label: "Tap on continue"
|
||||
point: "50%,92%"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
label: "Tap on Lets go"
|
||||
point: "50%,92%"
|
||||
@@ -1,127 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
|
||||
# Navigate to my profile
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
|
||||
# Can see feeds
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- assertVisible: "alice-favs"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: RIGHT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-0"
|
||||
|
||||
# Open and close edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileCancelBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
|
||||
# Edit display name and description via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileDisplayNameInput"
|
||||
- eraseText
|
||||
- inputText: "Alicia"
|
||||
- tapOn:
|
||||
id: "editProfileDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "One cool hacker"
|
||||
- tapOn: "Description"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible: "Alicia"
|
||||
- assertVisible: "One cool hacker"
|
||||
|
||||
# Remove display name and description via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileDisplayNameInput"
|
||||
- eraseText
|
||||
- tapOn:
|
||||
id: "editProfileDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Description"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible: "alice.test"
|
||||
- assertNotVisible: "One cool hacker"
|
||||
|
||||
# Set avi and banner via the edit profile modal
|
||||
- assertVisible:
|
||||
id: "userBannerFallback"
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "changeBannerBtn"
|
||||
- tapOn: "Upload from Library"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
id: "changeAvatarBtn"
|
||||
- tapOn: "Upload from Library"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible:
|
||||
id: "userBannerImage"
|
||||
|
||||
# # Remove avi and banner via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "changeBannerBtn"
|
||||
- tapOn: "Remove Banner"
|
||||
- tapOn:
|
||||
id: "changeAvatarBtn"
|
||||
- tapOn: "Remove Avatar"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible:
|
||||
id: "userBannerFallback"
|
||||
@@ -1,40 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Navigate to another user profile
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
# Can follow/unfollow another user
|
||||
- tapOn:
|
||||
id: "followBtn"
|
||||
- tapOn:
|
||||
id: "unfollowBtn"
|
||||
|
||||
# Can mute/unmute another user
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Mute Account"
|
||||
- assertVisible: "Account Muted"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Unmute Account"
|
||||
- assertNotVisible: "Account Muted"
|
||||
@@ -1,22 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Navigate to another user profile via autocomplete
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn: "/sys/debug"
|
||||
- tapOn:
|
||||
id: "sharedPrefsTestOpenBtn"
|
||||
- tapOn:
|
||||
id: "setStringBtn"
|
||||
- assertVisible: "Hello"
|
||||
- tapOn:
|
||||
id: "removeStringBtn"
|
||||
- assertVisible: "null"
|
||||
- tapOn:
|
||||
id: "setBoolBtn"
|
||||
- assertVisible: "true"
|
||||
- tapOn:
|
||||
id: "setNumberBtn"
|
||||
- assertVisible: "123"
|
||||
- tapOn:
|
||||
id: "addToSetBtn"
|
||||
- assertVisible: "true"
|
||||
- tapOn:
|
||||
id: "removeFromSetBtn"
|
||||
- assertVisible: "false"
|
||||
@@ -1,82 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
|
||||
|
||||
# Login, create a thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Test thread"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
# Login, reply to the thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInBob"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
# Login, confirm notification exists, mute thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Mute thread"
|
||||
|
||||
# Login, reply to the thread twice, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInBob"
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 3"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
|
||||
# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- assertNotVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
- tapOn: "Unmute thread"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "notifsFeed"
|
||||
direction: DOWN
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
@@ -1,76 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&thread"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
|
||||
# Navigate to thread
|
||||
- extendedWaitUntil:
|
||||
visible: "Thread root"
|
||||
- tapOn: "Thread root"
|
||||
- assertVisible: "Thread reply"
|
||||
|
||||
# Can like the root post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertVisible: "1 like"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertNotVisible: "1 like"
|
||||
|
||||
# Can like a reply post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
|
||||
# Can repost the root post
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Repost"
|
||||
- assertVisible:
|
||||
id: "repostCount-expanded"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount-expanded"
|
||||
|
||||
|
||||
# Can repost a reply post
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn: "Repost"
|
||||
- assertVisible:
|
||||
id: "repostCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
rootDir: '..',
|
||||
testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
|
||||
testTimeout: 120000,
|
||||
maxWorkers: 1,
|
||||
globalSetup: 'detox/runners/jest/globalSetup',
|
||||
globalTeardown: 'detox/runners/jest/globalTeardown',
|
||||
reporters: ['detox/runners/jest/reporter'],
|
||||
testEnvironment: 'detox/runners/jest/testEnvironment',
|
||||
verbose: true,
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# flow.yaml
|
||||
|
||||
appId: xyz.blueskyweb.app
|
||||
@@ -75,3 +74,4 @@ appId: xyz.blueskyweb.app
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {createServer as createHTTPServer} from 'node:http'
|
||||
import {parse} from 'node:url'
|
||||
|
||||
import {createServer, TestPDS} from '../jest/test-pds'
|
||||
|
||||
async function main() {
|
||||
@@ -15,7 +14,8 @@ async function main() {
|
||||
await server?.close()
|
||||
console.log('Starting new server')
|
||||
const inviteRequired = url?.query && 'invite' in url.query
|
||||
server = await createServer({inviteRequired})
|
||||
const phoneRequired = url?.query && 'phone' in url.query
|
||||
server = await createServer({inviteRequired, phoneRequired})
|
||||
console.log('Listening at', server.pdsUrl)
|
||||
if (url?.query) {
|
||||
if ('users' in url.query) {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
@@ -1,5 +0,0 @@
|
||||
// eslint-disable-next-line
|
||||
http.post('http://localhost:1986/' + SERVER_PATH, {
|
||||
headers: {'Content-Type': 'text/plain'},
|
||||
body: '',
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Composer', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Post text only', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await device.takeScreenshot('1- opened composer')
|
||||
await element(by.id('composerTextInput')).typeText('Post text only')
|
||||
await device.takeScreenshot('2- entered text')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await device.takeScreenshot('3- opened general section')
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Post with an image', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Post with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Post with a link card', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Post with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply text only', async () => {
|
||||
await element(by.id('e2eRefreshHome')).tap()
|
||||
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply text only')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply with an image', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply with a link card', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Reply with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP text only', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText('QP text only')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP with an image', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText('QP with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP with a link card', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'QP with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, createServer} from '../util'
|
||||
|
||||
describe('Create account', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can create a new account', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('5- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,211 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
|
||||
|
||||
describe('Curate lists', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&posts')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and create a curatelists', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('newUserListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Good Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They good')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They good')
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit curatelist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).clearText()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove description via the edit curatelist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('listDescription'))).not.toBeVisible()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Set avi via the edit curatelist modal', async () => {
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove avi via the edit curatelist modal', async () => {
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Delete the curatelist', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Delete List')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(element(by.id('listsEmpty'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Create a new curatelist', async () => {
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('newUserListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Good Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They good')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They good')
|
||||
})
|
||||
|
||||
it('Adds users on curatelists from the list', async () => {
|
||||
await element(by.text('About')).tap()
|
||||
await element(by.id('addUserBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).toBeVisible()
|
||||
await waitFor(element(by.id('user-bob.test-addBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('user-bob.test'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows posts by the users in the list', async () => {
|
||||
await element(by.text('Posts')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Pins the list', async () => {
|
||||
await expect(element(by.id('pinBtn'))).toBeVisible()
|
||||
await element(by.id('pinBtn')).tap()
|
||||
await element(by.id('e2eGotoHome')).tap()
|
||||
await element(by.id('homeScreenFeedTabs-Good Ppl')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
|
||||
await element(by.id('bottomBarFeedsBtn')).tap()
|
||||
await element(by.id('saved-feed-Good Ppl')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
|
||||
await element(by.id('unpinBtn')).tap()
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await expect(
|
||||
element(by.id('homeScreenFeedTabs-Good Ppl')),
|
||||
).not.toBeVisible()
|
||||
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Removes users on curatelists from the list', async () => {
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('user-bob.test'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-editBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows the curatelist on my profile', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-5')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Adds and removes users on curatelists from the profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a user list', async () => {
|
||||
await element(by.id('e2eGotoSettings')).tap()
|
||||
await element(by.id('signOutBtn')).tap()
|
||||
await loginAsBob()
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('alice')
|
||||
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-3')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Report List')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await expect(element(by.text('Report List'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Home screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&posts&feeds')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Can go to feeds page using feeds button in tab bar', async () => {
|
||||
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
|
||||
await expect(element(by.text('Discover new feeds'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Feeds button disappears after pinning a feed', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-4')).tap()
|
||||
await element(by.id('feed-alice-favs')).tap()
|
||||
await element(by.id('pinBtn')).tap()
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await expect(
|
||||
element(by.id('homeScreenFeedTabs-Feeds ✨')),
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can swipe between feeds', async () => {
|
||||
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
|
||||
await expect(element(by.id('customFeedPage'))).toBeVisible()
|
||||
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
|
||||
await expect(element(by.id('followingFeedPage'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can tap between feeds', async () => {
|
||||
await element(by.id('homeScreenFeedTabs-alice-favs')).tap()
|
||||
await expect(element(by.id('customFeedPage'))).toBeVisible()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await expect(element(by.id('followingFeedPage'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can delete posts', async () => {
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist()
|
||||
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Delete post')).tap()
|
||||
await expect(element(by.id('confirmModal'))).toBeVisible()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(
|
||||
element(alicePosts.withDescendant(by.text('Post'))),
|
||||
).not.toExist()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('invite-codes', () => {
|
||||
let service: string
|
||||
let inviteCode = ''
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users&invite')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can fetch invite codes', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eOpenInviteCodesModal')).tap()
|
||||
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
|
||||
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
|
||||
inviteCode = attrs.text
|
||||
await element(by.id('closeBtn')).tap()
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
})
|
||||
|
||||
it('I can create a new account with the invite code', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('inviteCodeInput')).typeText(inviteCode)
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('4- entered handle')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('invite-codes', () => {
|
||||
let service: string
|
||||
let inviteCode = ''
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users&invite&phone')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can fetch invite codes', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eOpenInviteCodesModal')).tap()
|
||||
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
|
||||
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
|
||||
inviteCode = attrs.text
|
||||
await element(by.id('closeBtn')).tap()
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
})
|
||||
|
||||
it('I can create a new account with the invite code', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('inviteCodeInput')).typeText(inviteCode)
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('phoneInput')).typeText('2345551234')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
await element(by.id('codeInput')).typeText('000000')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, login, createServer} from '../util'
|
||||
|
||||
describe('Login', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('As Alice, I can login', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
||||
await login(service, 'alice', 'hunter2', {
|
||||
takeScreenshots: true,
|
||||
})
|
||||
await device.takeScreenshot('5- opened home screen')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Mergefeed', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?mergefeed')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eToggleMergefeed')).tap()
|
||||
await element(by.id('bottomBarFeedsBtn')).tap()
|
||||
await element(by.id('feed-alice-favs-toggleSave')).tap()
|
||||
await element(by.id('e2eGotoHome')).tap()
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with default filters', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
// followed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'up',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with replies disabled', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap()
|
||||
await element(by.id('toggleRepliesBtn')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// followed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'up',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with no follows', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await element(by.id('profileHeaderBackBtn')).tap()
|
||||
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('searchTextInputClearBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('searchTextInputClearBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('carla')
|
||||
await element(by.id('searchAutoCompleteResult-carla.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await element(by.id('profileHeaderBackBtn')).tap()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// followed users NOT present
|
||||
await expect(element(by.id('feedItem-by-carla.test'))).not.toExist()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).not.toExist()
|
||||
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
|
||||
|
||||
describe('Mod lists', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&labels')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and view my modlists', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eGotoModeration')).tap()
|
||||
await element(by.id('moderationlistsBtn')).tap()
|
||||
await expect(element(by.id('list-Muted Users'))).toBeVisible()
|
||||
await element(by.id('list-Muted Users')).tap()
|
||||
await expect(
|
||||
element(by.id('user-muted-by-list-account.test')),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('Toggle mute subscription', async () => {
|
||||
await element(by.id('unmuteBtn')).tap()
|
||||
await element(by.id('subscribeBtn')).tap()
|
||||
await element(by.text('Mute accounts')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit modlist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).clearText()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove description via the edit modlist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('listDescription'))).not.toBeVisible()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
// DISABLED e2e environment is real finicky about avatar uploads -prf
|
||||
// it('Set avi via the edit modlist modal', async () => {
|
||||
// await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// await element(by.id('headerDropdownBtn')).tap()
|
||||
// await element(by.text('Edit list details')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
// await element(by.id('changeAvatarBtn')).tap()
|
||||
// await element(by.text('Library')).tap()
|
||||
// await sleep(3e3)
|
||||
// await element(by.id('saveBtn')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
// await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// // have to wait for the toast to clear
|
||||
// await waitFor(element(by.id('headerDropdownBtn')))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// })
|
||||
|
||||
// it('Remove avi via the edit modlist modal', async () => {
|
||||
// await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// await element(by.id('headerDropdownBtn')).tap()
|
||||
// await element(by.text('Edit list details')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
// await element(by.id('changeAvatarBtn')).tap()
|
||||
// await element(by.text('Remove')).tap()
|
||||
// await element(by.id('saveBtn')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
// await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// // have to wait for the toast to clear
|
||||
// await waitFor(element(by.id('headerDropdownBtn')))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// })
|
||||
|
||||
it('Delete the modlist', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Delete List')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(element(by.id('listsEmpty'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Create a new modlist', async () => {
|
||||
await element(by.id('newModListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
})
|
||||
|
||||
it('Adds and removes users on modlists from the list', async () => {
|
||||
await element(by.id('addUserBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).toBeVisible()
|
||||
await waitFor(element(by.id('user-warn-posts.test-addBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('user-warn-posts.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
|
||||
await element(by.id('listItems-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await expect(element(by.id('user-warn-posts.test'))).toBeVisible()
|
||||
await element(by.id('user-warn-posts.test-editBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-warn-posts.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows the modlist on my profile', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-5')).tap()
|
||||
await element(by.id('list-Bad Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Adds and removes users on modlists from the profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a mute list', async () => {
|
||||
await element(by.id('e2eGotoSettings')).tap()
|
||||
await element(by.id('signOutBtn')).tap()
|
||||
await loginAsBob()
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('alice')
|
||||
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-3')).tap()
|
||||
await element(by.id('list-Bad Ppl')).tap()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Report List')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await expect(element(by.text('Report List'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Profile screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&posts&feeds')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and navigate to my profile', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
})
|
||||
|
||||
it('Can see feeds', async () => {
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-4')).tap()
|
||||
await expect(element(by.id('feed-alice-favs'))).toBeVisible()
|
||||
await element(by.id('profilePager-selector')).swipe('right')
|
||||
await element(by.id('profilePager-selector-0')).tap()
|
||||
})
|
||||
|
||||
it('Open and close edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileCancelBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileDisplayNameInput')).clearText()
|
||||
await element(by.id('editProfileDisplayNameInput')).typeText('Alicia')
|
||||
await element(by.id('editProfileDescriptionInput')).clearText()
|
||||
await element(by.id('editProfileDescriptionInput')).typeText(
|
||||
'One cool hacker',
|
||||
)
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
||||
'Alicia',
|
||||
)
|
||||
await expect(element(by.id('profileHeaderDescription'))).toHaveText(
|
||||
'One cool hacker',
|
||||
)
|
||||
})
|
||||
|
||||
it('Remove display name and description via the edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileDisplayNameInput')).clearText()
|
||||
await element(by.id('editProfileDescriptionInput')).clearText()
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
||||
'alice.test',
|
||||
)
|
||||
await expect(element(by.id('profileHeaderDescription'))).not.toExist()
|
||||
})
|
||||
|
||||
it('Set avi and banner via the edit profile modal', async () => {
|
||||
await expect(element(by.id('userBannerFallback'))).toExist()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('changeBannerBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userBannerImage'))).toExist()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
})
|
||||
|
||||
it('Remove avi and banner via the edit profile modal', async () => {
|
||||
await expect(element(by.id('userBannerImage'))).toExist()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('changeBannerBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userBannerFallback'))).toExist()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
})
|
||||
|
||||
it('Navigate to another user profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('searchTextInput')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can follow/unfollow another user', async () => {
|
||||
await element(by.id('followBtn')).tap()
|
||||
await expect(element(by.id('unfollowBtn'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await expect(element(by.id('followBtn'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can mute/unmute another user', async () => {
|
||||
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Mute Account')).tap()
|
||||
await expect(element(by.id('profileHeaderAlert'))).toBeVisible()
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Unmute Account')).tap()
|
||||
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report another user', async () => {
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Report Account')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like posts', async () => {
|
||||
await element(by.id('postsFeed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost posts', async () => {
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report posts', async () => {
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Search screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
})
|
||||
|
||||
it('Navigate to another user profile via autocomplete', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Self-labeling', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Post an image with the porn label', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Post with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('labelsBtn')).tap()
|
||||
await element(by.id('pornLabelBtn')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
const posts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('e2eRefreshHome')).tap()
|
||||
await expect(
|
||||
element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0),
|
||||
).toExist()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Shell', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Can swipe the shelf open', async () => {
|
||||
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
|
||||
await expect(element(by.id('drawer'))).toBeVisible()
|
||||
await element(by.id('drawer')).swipe('left', 'fast', 0.75)
|
||||
await expect(element(by.id('drawer'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can open the shelf by pressing the header avi', async () => {
|
||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
||||
await expect(element(by.id('drawer'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can navigate using the shelf', async () => {
|
||||
await element(by.id('menuItemButton-Notifications')).tap()
|
||||
await expect(element(by.id('drawer'))).not.toBeVisible()
|
||||
await expect(element(by.id('notificationsScreen'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, createServer} from '../util'
|
||||
|
||||
describe('Create account', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?phone')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can create a new account with text verification', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('text-verification@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('phoneInput')).typeText('8042221111')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
|
||||
await element(by.id('codeInput')).typeText('000000')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('text-verification-test')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('failed text verification correctly goes back to the code input screen', async () => {
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('text-verification2@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('phoneInput')).typeText('8042221111')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
|
||||
await element(by.id('codeInput')).typeText('111111')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('text-verification-test2')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('codeInput'))).toBeVisible()
|
||||
await device.takeScreenshot('8- got error')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
|
||||
|
||||
describe('Thread muting', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login, create a thread, and log out', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Test thread')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Login, reply to the thread, and log out', async () => {
|
||||
await loginAsBob()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 1')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Login, confirm notification exists, mute thread, and log out', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
).toHaveText('Reply 1')
|
||||
await element(by.id('postDropdownBtn').withAncestor(bobNotifs))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Mute thread')).tap()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('viewHeaderDrawerBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Login, reply to the thread twice, and log out', async () => {
|
||||
await loginAsBob()
|
||||
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector-1')).tap()
|
||||
const bobPosts = by.id('feedItem-by-bob.test')
|
||||
await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 2')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 3')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
})
|
||||
|
||||
it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
|
||||
await loginAsAlice()
|
||||
|
||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
).not.toExist()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Unmute thread')).tap()
|
||||
|
||||
// TODO
|
||||
// the swipe down to trigger PTR isnt working and I dont want to block on this
|
||||
// -prf
|
||||
// await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
// await element(by.id('notifsFeed')).swipe('down', 'fast')
|
||||
// await waitFor(element(by.id('postText').withAncestor(bobNotifs)))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
// ).toHaveText('Reply 2')
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(1),
|
||||
// ).toHaveText('Reply 3')
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(2),
|
||||
// ).toHaveText('Reply 1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Thread screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&thread')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login & navigate to thread', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
|
||||
await expect(
|
||||
element(
|
||||
by
|
||||
.id('postThreadItem-by-bob.test')
|
||||
.withDescendant(by.text('Thread root')),
|
||||
),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
element(
|
||||
by
|
||||
.id('postThreadItem-by-carla.test')
|
||||
.withDescendant(by.text('Thread reply')),
|
||||
),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1 like')
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can like a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1 repost')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import {resolveConfig} from 'detox/internals'
|
||||
import {execSync} from 'child_process'
|
||||
import http from 'http'
|
||||
|
||||
const platform = device.getPlatform()
|
||||
|
||||
export async function openApp(opts: any) {
|
||||
opts = opts || {}
|
||||
const config = await resolveConfig()
|
||||
|
||||
if (device.getPlatform() === 'ios') {
|
||||
// disable password autofill
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
|
||||
)
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
|
||||
)
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
|
||||
)
|
||||
}
|
||||
if (config.configurationName.split('.').includes('debug')) {
|
||||
return await openAppForDebugBuild(platform, opts)
|
||||
} else {
|
||||
return await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function isVisible(id: string) {
|
||||
try {
|
||||
await expect(element(by.id(id))).toBeVisible()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(
|
||||
service: string,
|
||||
username: string,
|
||||
password: string,
|
||||
{takeScreenshots} = {takeScreenshots: false},
|
||||
) {
|
||||
await element(by.id('signInButton')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('1- opened sign-in screen')
|
||||
}
|
||||
if (await isVisible('chooseAccountForm')) {
|
||||
await element(by.id('chooseNewAccountBtn')).tap()
|
||||
}
|
||||
await element(by.id('loginSelectServiceButton')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('2- opened service selector')
|
||||
}
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('3- input custom service')
|
||||
}
|
||||
await element(by.id('loginUsernameInput')).typeText(username)
|
||||
await element(by.id('loginPasswordInput')).typeText(password)
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('4- entered username and password')
|
||||
}
|
||||
await element(by.id('loginNextButton')).tap()
|
||||
}
|
||||
|
||||
export async function loginAsAlice() {
|
||||
await element(by.id('e2eSignInAlice')).tap()
|
||||
}
|
||||
|
||||
export async function loginAsBob() {
|
||||
await element(by.id('e2eSignInBob')).tap()
|
||||
}
|
||||
|
||||
async function openAppForDebugBuild(platform: string, opts: any) {
|
||||
const deepLinkUrl = // Local testing with packager
|
||||
/*process.env.EXPO_USE_UPDATES
|
||||
? // Testing latest published EAS update for the test_debug channel
|
||||
getDeepLinkUrl(getLatestUpdateUrl())
|
||||
: */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform))
|
||||
|
||||
if (platform === 'ios') {
|
||||
await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
})
|
||||
sleep(3000)
|
||||
await device.openURL({
|
||||
url: deepLinkUrl,
|
||||
})
|
||||
} else {
|
||||
await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
url: deepLinkUrl,
|
||||
})
|
||||
}
|
||||
|
||||
await sleep(3000)
|
||||
}
|
||||
|
||||
export async function createServer(path = ''): Promise<string> {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var req = http.request(
|
||||
{
|
||||
method: 'POST',
|
||||
host: 'localhost',
|
||||
port: 1986,
|
||||
path: `/${path}`,
|
||||
},
|
||||
function (res) {
|
||||
const body: Buffer[] = []
|
||||
res.on('data', chunk => body.push(chunk))
|
||||
res.on('end', function () {
|
||||
try {
|
||||
resolve(Buffer.concat(body).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
const getDeepLinkUrl = (url: string) =>
|
||||
`expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}`
|
||||
|
||||
const getDevLauncherPackagerUrl = (platform: string) =>
|
||||
`http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`
|
||||
|
||||
export const sleep = (t: number) => new Promise(res => setTimeout(res, t))
|
||||
@@ -0,0 +1,47 @@
|
||||
export const exampleComHtml = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Example Domain</title>
|
||||
<meta name="description" content="An example website">
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #f0f0f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
}
|
||||
div {
|
||||
width: 600px;
|
||||
margin: 5em auto;
|
||||
padding: 2em;
|
||||
background-color: #fdfdff;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
|
||||
}
|
||||
a:link, a:visited {
|
||||
color: #38488f;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
div {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>Example Domain</h1>
|
||||
<p>This domain is for use in illustrative examples in documents. You may use this
|
||||
domain in literature without prior coordination or asking for permission.</p>
|
||||
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,11 +9,11 @@ describe('isNetworkError', () => {
|
||||
]
|
||||
const outputs = [true, false, false, true]
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = outputs[i]
|
||||
it(`correctly distinguishes network errors for ${input}`, () => {
|
||||
expect(isNetworkError(input)).toEqual(output)
|
||||
})
|
||||
}
|
||||
it('correctly distinguishes network errors', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = isNetworkError(input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import {extractHtmlMeta} from '../../src/lib/link-meta/html'
|
||||
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
||||
import {youtubeHTML} from './__mocks__/youtubeHtml'
|
||||
import {tiktokHtml} from './__mocks__/tiktokHtml'
|
||||
import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml'
|
||||
|
||||
describe('extractHtmlMeta', () => {
|
||||
const cases = [
|
||||
['', {}],
|
||||
['nothing', {}],
|
||||
['<title>title</title>', {title: 'title'}],
|
||||
['<title> aSd!@#AC </title>', {title: 'aSd!@#AC'}],
|
||||
['<title>\n title\n </title>', {title: 'title'}],
|
||||
['<meta name="title" content="meta title">', {title: 'meta title'}],
|
||||
[
|
||||
'<meta name="description" content="meta description">',
|
||||
{description: 'meta description'},
|
||||
],
|
||||
['<meta property="og:title" content="og title">', {title: 'og title'}],
|
||||
[
|
||||
'<meta property="og:description" content="og description">',
|
||||
{description: 'og description'},
|
||||
],
|
||||
[
|
||||
'<meta property="og:image" content="https://ogimage.com/foo.png">',
|
||||
{image: 'https://ogimage.com/foo.png'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:title" content="twitter title">',
|
||||
{title: 'twitter title'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:description" content="twitter description">',
|
||||
{description: 'twitter description'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:image" content="https://twitterimage.com/foo.png">',
|
||||
{image: 'https://twitterimage.com/foo.png'},
|
||||
],
|
||||
['<meta\n name="title"\n content="meta title"\n>', {title: 'meta title'}],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
'given the html tag %p, returns %p',
|
||||
// @ts-ignore not worth fixing -prf
|
||||
(input, expectedResult) => {
|
||||
const output = extractHtmlMeta({html: input as string, hostname: ''})
|
||||
expect(output).toEqual(expectedResult)
|
||||
},
|
||||
)
|
||||
|
||||
it('extracts title and description from a generic HTML page', () => {
|
||||
const input = exampleComHtml
|
||||
const expectedOutput = {
|
||||
title: 'Example Domain',
|
||||
description: 'An example website',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'example.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts title and description from a Tiktok HTML page', () => {
|
||||
const input = tiktokHtml
|
||||
const expectedOutput = {
|
||||
title:
|
||||
'Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok',
|
||||
description:
|
||||
'5.5M Likes, 20.8K Comments. TikTok video from Power Vision Tests (@_powervision_): "Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp". оригинальный звук - Power Vision Tests.',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'tiktok.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts title and description from a generic youtube page', () => {
|
||||
const input = youtubeHTML
|
||||
const expectedOutput = {
|
||||
title: 'HD Video (1080p) with Relaxing Music of Native American Shamans',
|
||||
description:
|
||||
'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ',
|
||||
image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts avatar from a youtube channel', () => {
|
||||
const input = youtubeChannelHtml
|
||||
const expectedOutput = {
|
||||
title: 'penguinz0',
|
||||
description:
|
||||
'Clips channel: https://www.youtube.com/channel/UC4EQHfzIbkL_Skit_iKt1aA\n\nTwitter: https://twitter.com/MoistCr1TiKaL\n\nInstagram: https://www.instagram.com/bigmoistcr1tikal/?hl=en\n\nTwitch: https://www.twitch.tv/moistcr1tikal\n\nSnapchat: Hugecharles\n\nTik Tok: Hugecharles\n\nI don't have any other public accounts.',
|
||||
image:
|
||||
'https://yt3.googleusercontent.com/ytc/AL5GRJWOhJOuUC6C2b7gP-5D2q6ypXbcOOckyAE1En4RUQ=s176-c-k-c0x00ffffff-no-rj',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts username from the url a twitter profile page', () => {
|
||||
const expectedOutput = {
|
||||
title: '@bluesky on Twitter',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/bluesky',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts username from the url a tweet', () => {
|
||||
const expectedOutput = {
|
||||
title: 'Tweet by @bluesky',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/bluesky/status/1582437529969917953',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it("does not extract username from the url when it's not a tweet or profile page", () => {
|
||||
const expectedOutput = {
|
||||
title: 'Twitter',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/i/articles/follows/-1675653703?time_window=24',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
})
|
||||
@@ -1,30 +1,25 @@
|
||||
import {deleteAsync} from 'expo-file-system'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
getResizedDimensions,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
const mockResizedImage = {
|
||||
path: 'file://resized-image.jpg',
|
||||
size: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
|
||||
const mockResizedImage = {
|
||||
path: jest.fn().mockReturnValue('file://resized-image.jpg'),
|
||||
size: 100,
|
||||
width: 50,
|
||||
height: 50,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedCreateResizedImage = manipulateAsync as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue({
|
||||
uri: 'file://resized-image.jpg',
|
||||
...mockResizedImage,
|
||||
})
|
||||
const mockedCreateResizedImage =
|
||||
ImageResizer.createResizedImage as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue(mockResizedImage)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -35,7 +30,6 @@ describe('downloadAndResize', () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
@@ -58,17 +52,17 @@ describe('downloadAndResize', () => {
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
)
|
||||
|
||||
// First time it gets called is to get dimensions
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {})
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[{resize: {height: opts.height, width: opts.width}}],
|
||||
{format: SaveFormat.JPEG, compress: 1.0},
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image.jpg',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
)
|
||||
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
|
||||
idempotent: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined for invalid URI', async () => {
|
||||
@@ -86,11 +80,10 @@ describe('downloadAndResize', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
it('should return undefined for unsupported file type', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
@@ -104,47 +97,25 @@ describe('downloadAndResize', () => {
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 1200,
|
||||
height: 1000,
|
||||
}
|
||||
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
|
||||
|
||||
const initialDimensionsTwo = {
|
||||
width: 1000,
|
||||
height: 1200,
|
||||
}
|
||||
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
|
||||
|
||||
expect(resizedDimensionsOne).toEqual(initialDimensionsOne)
|
||||
expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo)
|
||||
})
|
||||
|
||||
it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 3000,
|
||||
height: 1500,
|
||||
}
|
||||
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
|
||||
|
||||
const initialDimensionsTwo = {
|
||||
width: 2000,
|
||||
height: 4000,
|
||||
}
|
||||
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
|
||||
|
||||
expect(resizedDimensionsOne).toEqual({
|
||||
width: 2000,
|
||||
height: 1000,
|
||||
})
|
||||
expect(resizedDimensionsTwo).toEqual({
|
||||
width: 1000,
|
||||
height: 2000,
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+92
-321
@@ -1,22 +1,17 @@
|
||||
import {RichText} from '@atproto/api'
|
||||
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
import {
|
||||
createStarterPackGooglePlayUri,
|
||||
createStarterPackLinkFromAndroidReferrer,
|
||||
parseStarterPackUri,
|
||||
} from 'lib/strings/starter-pack'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
|
||||
import {enforceLen} from '../../src/lib/strings/helpers'
|
||||
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
||||
import {
|
||||
makeRecordUri,
|
||||
toNiceDomain,
|
||||
toShareUrl,
|
||||
toShortUrl,
|
||||
toShareUrl,
|
||||
} from '../../src/lib/strings/url-helpers'
|
||||
import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
|
||||
import {ago} from '../../src/lib/strings/time'
|
||||
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
||||
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
|
||||
describe('detectLinkables', () => {
|
||||
const inputs = [
|
||||
@@ -131,6 +126,35 @@ describe('detectLinkables', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluralize', () => {
|
||||
const inputs: [number, string, string?][] = [
|
||||
[1, 'follower'],
|
||||
[1, 'member'],
|
||||
[100, 'post'],
|
||||
[1000, 'repost'],
|
||||
[10000, 'upvote'],
|
||||
[100000, 'other'],
|
||||
[2, 'man', 'men'],
|
||||
]
|
||||
const outputs = [
|
||||
'follower',
|
||||
'member',
|
||||
'posts',
|
||||
'reposts',
|
||||
'upvotes',
|
||||
'others',
|
||||
'men',
|
||||
]
|
||||
|
||||
it('correctly pluralizes a set of words', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = pluralize(...input)
|
||||
expect(output).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRecordUri', () => {
|
||||
const inputs: [string, string, string][] = [
|
||||
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
|
||||
@@ -146,6 +170,36 @@ describe('makeRecordUri', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ago', () => {
|
||||
const inputs = [
|
||||
1671461038,
|
||||
'04 Dec 1995 00:12:00 GMT',
|
||||
new Date(),
|
||||
new Date().setSeconds(new Date().getSeconds() - 10),
|
||||
new Date().setMinutes(new Date().getMinutes() - 10),
|
||||
new Date().setHours(new Date().getHours() - 1),
|
||||
new Date().setDate(new Date().getDate() - 1),
|
||||
new Date().setMonth(new Date().getMonth() - 1),
|
||||
]
|
||||
const outputs = [
|
||||
new Date(1671461038).toLocaleDateString(),
|
||||
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
|
||||
'now',
|
||||
'10s',
|
||||
'10m',
|
||||
'1h',
|
||||
'1d',
|
||||
'1mo',
|
||||
]
|
||||
|
||||
it('correctly calculates how much time passed, in a string', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = ago(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeValidHandle', () => {
|
||||
const inputs = [
|
||||
'test-handle-123',
|
||||
@@ -340,14 +394,11 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://youtube.com/watch?v=videoId',
|
||||
'https://youtube.com/watch?v=videoId&feature=share',
|
||||
'https://youtube.com/shorts/videoId',
|
||||
'https://youtube.com/live/videoId',
|
||||
'https://m.youtube.com/watch?v=videoId',
|
||||
'https://music.youtube.com/watch?v=videoId',
|
||||
|
||||
'https://youtube.com/shorts/',
|
||||
'https://youtube.com/',
|
||||
'https://youtube.com/random',
|
||||
'https://youtube.com/live/',
|
||||
|
||||
'https://twitch.tv/channelName',
|
||||
'https://www.twitch.tv/channelName',
|
||||
@@ -383,8 +434,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://giphy.com/gif/some-random-gif-name-gifId',
|
||||
'https://giphy.com/gifs/',
|
||||
|
||||
'https://giphy.com/gifs/39248209509382934029?hh=100&ww=100',
|
||||
|
||||
'https://media.giphy.com/media/gifId/giphy.webp',
|
||||
'https://media0.giphy.com/media/gifId/giphy.webp',
|
||||
'https://media1.giphy.com/media/gifId/giphy.gif',
|
||||
@@ -407,32 +456,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://tenor.com/view',
|
||||
'https://tenor.com/view/gifId.gif',
|
||||
'https://tenor.com/intl/view/gifId.gif',
|
||||
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif?hh=100&ww=100',
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif',
|
||||
'https://media.tenor.com/someID/someName.gif',
|
||||
'https://media.tenor.com/someID',
|
||||
'https://media.tenor.com',
|
||||
|
||||
'https://www.flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661/',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661//',
|
||||
'https://flic.kr/s/aHBqjAES3i',
|
||||
|
||||
'https://flickr.com/foetoes/username/albums/3903',
|
||||
'https://flickr.com/albums/3903',
|
||||
'https://flic.kr/s/OolI',
|
||||
'https://flic.kr/t/aHBqjAES3i',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool/',
|
||||
'https://flickr.com/groups/898944@N23/pool//',
|
||||
'https://flic.kr/go/8WJtR',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/',
|
||||
'https://www.flickr.com/groups',
|
||||
]
|
||||
|
||||
const outputs = [
|
||||
@@ -472,21 +495,10 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'twitch_video',
|
||||
@@ -609,17 +621,18 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/39248209509382934029',
|
||||
playerUri: 'https://i.giphy.com/media/39248209509382934029/200.webp',
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -627,7 +640,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -635,7 +648,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -643,7 +656,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -651,7 +664,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -659,15 +672,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -679,7 +684,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
|
||||
{
|
||||
@@ -688,7 +693,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -696,7 +701,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -704,7 +709,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -712,90 +717,32 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://t.gifs.bsky.app/someID_AAAAM/someName.gif',
|
||||
dimensions: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://tenor.com/intl/view/gifId.gif',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
|
||||
it('correctly grabs the correct id from uri', () => {
|
||||
@@ -809,179 +756,3 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStarterPackLinkFromAndroidReferrer', () => {
|
||||
const validOutput = 'at://haileyok.com/app.bsky.graph.starterpack/rkey'
|
||||
|
||||
it('returns a link when input contains utm_source and utm_content', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_content=starterpack_test-lover-9000.com_rkey',
|
||||
),
|
||||
).toEqual('at://test-lover-9000.com/app.bsky.graph.starterpack/rkey')
|
||||
})
|
||||
|
||||
it('returns a link when input contains utm_source and utm_content in different order', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
})
|
||||
|
||||
it('returns a link when input contains other parameters as well', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
})
|
||||
|
||||
it('returns null when utm_source is not present', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when utm_content is not present', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'),
|
||||
).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when utm_content is malformed', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com',
|
||||
),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey_more',
|
||||
),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=notastarterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseStarterPackHttpUri', () => {
|
||||
const baseUri = 'https://bsky.app/start'
|
||||
|
||||
it('returns a valid at uri when http uri is valid', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com/rkey`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting`
|
||||
expect(parseStarterPackUri(validHttpUri2)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'ilovetesting',
|
||||
})
|
||||
|
||||
const validHttpUri3 = `${baseUri}/testlover9000.com/rkey`
|
||||
expect(parseStarterPackUri(validHttpUri3)).toEqual({
|
||||
name: 'testlover9000.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when there is no rkey', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when there is an extra path', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com/rkey/other`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when there is no handle or rkey', () => {
|
||||
const validHttpUri = `${baseUri}`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the route is not /start or /starter-pack', () => {
|
||||
const validHttpUri = 'https://bsky.app/start/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const validHttpUri2 = 'https://bsky.app/starter-pack/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(validHttpUri2)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const invalidHttpUri = 'https://bsky.app/profile/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(invalidHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns the at uri when the input is a valid starterpack at uri', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual({
|
||||
name: 'did:123',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when the at uri has no rkey', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.starterpack'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the collection is not app.bsky.graph.starterpack', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.list/rkey'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the input is undefined', () => {
|
||||
expect(parseStarterPackUri(undefined)).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStarterPackGooglePlayUri', () => {
|
||||
const base =
|
||||
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_'
|
||||
|
||||
it('returns valid google play uri when input is valid', () => {
|
||||
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
|
||||
`${base}name_rkey`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null when no rkey is supplied', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when no name or rkey are supplied', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when rkey is supplied but no name', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
import {it, describe, expect} from '@jest/globals'
|
||||
|
||||
import {
|
||||
isPossiblyAUrl,
|
||||
isTrustedUrl,
|
||||
linkRequiresWarning,
|
||||
isPossiblyAUrl,
|
||||
splitApexDomain,
|
||||
} from '../../../src/lib/strings/url-helpers'
|
||||
|
||||
@@ -75,10 +74,6 @@ describe('linkRequiresWarning', () => {
|
||||
// bad uri inputs, default to true
|
||||
['', '', true],
|
||||
['example.com', 'example.com', true],
|
||||
['/profile', 'Username', false],
|
||||
['#', 'Show More', false],
|
||||
['https://docs.bsky.app', 'https://docs.bsky.app', false],
|
||||
['https://bsky.app/compose/intent?text=test', 'Compose a post', false],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
@@ -144,37 +139,3 @@ describe('splitApexDomain', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('isTrustedUrl', () => {
|
||||
const cases = [
|
||||
['#', true],
|
||||
['#profile', true],
|
||||
['/', true],
|
||||
['/profile', true],
|
||||
['/profile/', true],
|
||||
['/profile/bob.test', true],
|
||||
['https://bsky.app', true],
|
||||
['https://bsky.app/', true],
|
||||
['https://bsky.app/profile/bob.test', true],
|
||||
['https://www.bsky.app', true],
|
||||
['https://www.bsky.app/', true],
|
||||
['https://docs.bsky.app', true],
|
||||
['https://bsky.social', true],
|
||||
['https://bsky.social/blog', true],
|
||||
['https://blueskyweb.xyz', true],
|
||||
['https://blueskyweb.zendesk.com', true],
|
||||
['http://bsky.app', true],
|
||||
['http://bsky.social', true],
|
||||
['http://blueskyweb.xyz', true],
|
||||
['http://blueskyweb.zendesk.com', true],
|
||||
['https://google.com', false],
|
||||
['https://docs.google.com', false],
|
||||
['https://google.com/#', false],
|
||||
['https://blueskywebxzendesk.com', false],
|
||||
]
|
||||
|
||||
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
|
||||
const output = isTrustedUrl(str)
|
||||
expect(output).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
+24
-131
@@ -11,17 +11,6 @@ const DARK_SPLASH_CONFIG = {
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
const SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0c7cff',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
const DARK_SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0f141b',
|
||||
image: './assets/splash-dark.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
module.exports = function (config) {
|
||||
/**
|
||||
* App version number. Should be incremented as part of a release cycle.
|
||||
@@ -35,30 +24,10 @@ module.exports = function (config) {
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
|
||||
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
|
||||
|
||||
const ASSOCIATED_DOMAINS = [
|
||||
'applinks:bsky.app',
|
||||
'applinks:staging.bsky.app',
|
||||
'appclips:bsky.app',
|
||||
'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes
|
||||
// When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port.
|
||||
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||
]
|
||||
|
||||
const UPDATES_CHANNEL = IS_TESTFLIGHT
|
||||
? 'testflight'
|
||||
: IS_PRODUCTION
|
||||
? 'production'
|
||||
: undefined
|
||||
const UPDATES_ENABLED = !!UPDATES_CHANNEL
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
|
||||
IS_DEV ? 'dev' : ''
|
||||
}`
|
||||
const DIST_BUILD_NUMBER =
|
||||
PLATFORM === 'android'
|
||||
? process.env.BSKY_ANDROID_VERSION_CODE
|
||||
: process.env.BSKY_IOS_BUILD_NUMBER
|
||||
|
||||
return {
|
||||
expo: {
|
||||
@@ -74,8 +43,6 @@ module.exports = function (config) {
|
||||
icon: './assets/icon.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
splash: SPLASH_CONFIG,
|
||||
// hsl(211, 99%, 53%), same as palette.default.brandText
|
||||
primaryColor: '#1083fe',
|
||||
ios: {
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
@@ -93,7 +60,7 @@ module.exports = function (config) {
|
||||
NSPhotoLibraryUsageDescription:
|
||||
'Used for profile pictures, posts, and other kinds of content',
|
||||
},
|
||||
associatedDomains: ASSOCIATED_DOMAINS,
|
||||
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
|
||||
splash: {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
@@ -101,38 +68,10 @@ module.exports = function (config) {
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryFileTimestamp',
|
||||
NSPrivacyAccessedAPITypeReasons: ['C617.1', '3B52.1', '0A2A.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
|
||||
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategorySystemBootTime',
|
||||
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryUserDefaults',
|
||||
NSPrivacyAccessedAPITypeReasons: ['CA92.1', '1C8F.1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'light-content',
|
||||
backgroundColor: '#00000000',
|
||||
},
|
||||
// Dark nav bar in light mode is better than light nav bar in dark mode
|
||||
androidNavigationBar: {
|
||||
barStyle: 'light-content',
|
||||
backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
|
||||
barStyle: 'dark-content',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/icon.png',
|
||||
@@ -153,7 +92,7 @@ module.exports = function (config) {
|
||||
scheme: 'https',
|
||||
host: 'bsky.app',
|
||||
},
|
||||
IS_DEV && {
|
||||
{
|
||||
scheme: 'http',
|
||||
host: 'localhost:19006',
|
||||
},
|
||||
@@ -162,83 +101,50 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
splash: {
|
||||
...SPLASH_CONFIG_ANDROID,
|
||||
dark: DARK_SPLASH_CONFIG_ANDROID,
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
},
|
||||
},
|
||||
web: {
|
||||
favicon: './assets/favicon.png',
|
||||
},
|
||||
updates: {
|
||||
url: 'https://updates.bsky.app/manifest',
|
||||
enabled: UPDATES_ENABLED,
|
||||
fallbackToCacheTimeout: 30000,
|
||||
codeSigningCertificate: UPDATES_ENABLED
|
||||
? './code-signing/certificate.pem'
|
||||
: undefined,
|
||||
codeSigningMetadata: UPDATES_ENABLED
|
||||
? {
|
||||
keyid: 'main',
|
||||
alg: 'rsa-v1_5-sha256',
|
||||
}
|
||||
: undefined,
|
||||
checkAutomatically: 'NEVER',
|
||||
channel: UPDATES_CHANNEL,
|
||||
enabled: true,
|
||||
fallbackToCacheTimeout: 1000,
|
||||
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
plugins: [
|
||||
'expo-localization',
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
{
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
release: VERSION,
|
||||
dist: SENTRY_DIST,
|
||||
},
|
||||
],
|
||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||
[
|
||||
'expo-build-properties',
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '15.1',
|
||||
newArchEnabled: false,
|
||||
deploymentTarget: '13.4',
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-updates',
|
||||
{
|
||||
username: 'blueskysocial',
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-notifications',
|
||||
{
|
||||
icon: './assets/icon-android-notification.png',
|
||||
color: '#1185fe',
|
||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||
color: '#ffffff',
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
[
|
||||
'expo-font',
|
||||
{
|
||||
fonts: [
|
||||
'./assets/fonts/inter/InterVariable.ttf',
|
||||
'./assets/fonts/inter/InterVariable-Italic.ttf',
|
||||
],
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
@@ -255,19 +161,6 @@ module.exports = function (config) {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetName: 'BlueskyNSE',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE',
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': [
|
||||
'group.app.bsky',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetName: 'BlueskyClip',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.AppClip',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -281,12 +174,12 @@ module.exports = function (config) {
|
||||
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
|
||||
*/
|
||||
{
|
||||
file: './postHooks/uploadSentrySourcemapsPostHook',
|
||||
file: 'sentry-expo/upload-sourcemaps',
|
||||
config: {
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
release: VERSION,
|
||||
dist: SENTRY_DIST,
|
||||
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-2.86.26.014.002c.944.125 1.893.238 2.846.238.95 0 1.904-.113 2.846-.238l.014-.002h.003a1 1 0 0 1 .273 1.98l-.006.002-.017.002a33 33 0 0 1-2.014.21c.195 1.32.65 2.33 1.626 3.357a1 1 0 0 1-1.45 1.378 8.3 8.3 0 0 1-1.234-1.647 8.2 8.2 0 0 1-1.342 1.673 1 1 0 0 1-1.398-1.43c.673-.658 1.088-1.274 1.342-1.922.163-.42.269-.878.32-1.404a33 33 0 0 1-2.075-.215l-.017-.002-.006-.001a1 1 0 0 1 .271-1.982l.004.001Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 700 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 11a7 7 0 0 1 14 0c0 2.625-1.547 5.138-3.354 7.066a17 17 0 0 1-2.55 2.242 8 8 0 0 1-.924.577 3 3 0 0 1-.172.083 3 3 0 0 1-.172-.083 8 8 0 0 1-.923-.577 17 17 0 0 1-2.55-2.242C6.547 16.138 5 13.625 5 11Zm6.882 10.012Zm.232-.001h-.003l.007.001-.004-.001ZM12 2a9 9 0 0 0-9 9c0 3.375 1.953 6.362 3.895 8.434a19 19 0 0 0 2.856 2.508c.425.3.82.545 1.159.72.168.087.337.164.498.222.14.05.356.116.592.116s.451-.066.592-.116c.16-.058.33-.135.498-.222.339-.175.734-.42 1.159-.72.85-.6 1.87-1.457 2.856-2.508C19.047 17.362 21 14.375 21 11a9 9 0 0 0-9-9ZM7.38 9.927c2.774-.094 3.459 1.31 3.591 3.19a.89.89 0 0 1-.855.956c-2.774.094-3.458-1.31-3.59-3.19a.89.89 0 0 1 .854-.956Zm9.236 0c-2.774-.094-3.458 1.31-3.59 3.19a.89.89 0 0 0 .854.956c2.774.094 3.459-1.31 3.591-3.19a.89.89 0 0 0-.855-.956Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 928 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.148 2.034a1 1 0 0 1 1.35-.419c1.054.556 1.873 1.266 2.46 2.174.369.57.63 1.197.807 1.873 1.143-.349 2.194-.46 3.15-.342a5.12 5.12 0 0 1 3.134 1.556c1.564 1.63 2.086 4.183 1.922 6.58-.166 2.414-1.043 4.938-2.607 6.619-.792.851-1.78 1.505-2.95 1.782-1.063.251-2.213.176-3.415-.262-1.202.438-2.351.513-3.414.262-1.17-.277-2.158-.93-2.95-1.782-1.563-1.682-2.44-4.205-2.606-6.62-.164-2.396.358-4.949 1.921-6.579A5.12 5.12 0 0 1 8.085 5.32q1.166-.144 2.518.172a4 4 0 0 0-.325-.618c-.372-.576-.912-1.068-1.712-1.49a1 1 0 0 1-.418-1.35Zm.897 17.877c.71.167 1.557.117 2.562-.31a1 1 0 0 1 .784 0c1.005.428 1.853.477 2.563.31.715-.17 1.37-.579 1.946-1.198 1.171-1.26 1.932-3.303 2.076-5.394.144-2.109-.353-3.998-1.37-5.059a3.12 3.12 0 0 0-1.936-.955c-.83-.102-1.912.043-3.282.621a1 1 0 0 1-.778 0c-1.37-.578-2.45-.723-3.281-.62-.815.1-1.445.443-1.935.954-1.017 1.06-1.514 2.95-1.37 5.059.144 2.09.905 4.135 2.076 5.394.575.62 1.23 1.029 1.945 1.198Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 285 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 402 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user