Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Bailey 86426b528a WIP quote embed hover 2024-04-12 19:26:00 -05:00
1149 changed files with 63206 additions and 148095 deletions
+86
View File
@@ -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
View File
@@ -95,7 +95,8 @@ web-build/
/ios/
# environment variables
.env
.env.*
# Firebase (Android) Google services
# INCLUDED: google-services.json
# INCLUDED: google-services.json
-2
View File
@@ -1,2 +0,0 @@
[*.{kt,kts}]
indent_size=2
-2
View File
@@ -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 -17
View File
@@ -9,11 +9,11 @@ module.exports = {
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'detox',
'react',
'lingui',
'simple-import-sort',
'bsky-internal',
'eslint-plugin-react-compiler',
],
rules: {
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
@@ -31,7 +31,6 @@ module.exports = {
},
},
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'simple-import-sort/imports': [
'warn',
@@ -69,21 +68,6 @@ module.exports = {
},
],
'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',
@@ -1,9 +1,9 @@
name: build-and-push-bskyweb-aws
on:
workflow_dispatch:
push:
branches:
- main
- 3p-moderators
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
@@ -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
+1 -50
View File
@@ -25,8 +25,6 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -34,9 +32,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,8 +57,6 @@ 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
@@ -99,17 +92,13 @@ jobs:
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 }}```"
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
@@ -126,41 +115,3 @@ jobs:
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
-39
View File
@@ -25,8 +25,6 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -34,9 +32,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 +45,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,8 +65,6 @@ 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
@@ -83,31 +72,3 @@ jobs:
- 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
+61 -63
View File
@@ -26,7 +26,7 @@ jobs:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
outputs:
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
steps:
- name: Check for EXPO_TOKEN
@@ -49,22 +49,67 @@ jobs:
with:
fetch-depth: 0
- name: ⬇️ Get last successful deployment commit from the cache
id: get-base-commit
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}
- name: Add the last successful deployment commit to the output
id: last-successful-commit
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
- name: ⬇️ Fetch commits from base branch
if: ${{ github.ref != 'refs/heads/main' }}
run: git fetch origin main:main --depth 100
# This should get the current production release's commit's hash to see if the update is compatible
- name: 🕵️ Get the base commit
id: base-commit
run: |
if [ -z "${{ inputs.channel == 'production' }}" ]; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: 📷 Check fingerprint and install dependencies
- name: ⚙️ Install Dependencies
run: yarn install
# Run the fingerprint
- name: 📷 Check fingerprint
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@main
uses: expo/expo-github-action/fingerprint@main
with:
profile: ${{ inputs.channel || 'testflight' }}
previous-commit-tag: ${{ inputs.runtimeVersion }}
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
- name: 👀 Debug fingerprint
id: fingerprint-debug
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
fi
- name: Lint check
run: yarn lint
@@ -80,52 +125,41 @@ jobs:
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
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 }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
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
- name: Save successful deployment commit hash
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
@@ -133,11 +167,11 @@ jobs:
runs-on: macos-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: true
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 }}
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -148,8 +182,6 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -170,10 +202,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:
@@ -194,8 +222,6 @@ 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
@@ -204,18 +230,6 @@ jobs:
- 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
@@ -225,7 +239,7 @@ jobs:
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 }}
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
@@ -237,8 +251,6 @@ jobs:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 5
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -271,8 +283,6 @@ 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
@@ -303,15 +313,3 @@ jobs:
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
+6 -6
View File
@@ -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/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/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 -11
View File
@@ -1,7 +1,7 @@
# Credit for fingerprint action https://github.com/expo/expo
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
---
name: PR Tests
name: PR labeler
on:
push:
@@ -21,7 +21,7 @@ permissions:
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -94,9 +94,10 @@ jobs:
| ${{ 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:
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -113,23 +114,35 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: 📷 Check fingerprint and install dependencies
- name: ⚙️ Install Dependencies
run: yarn install
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: 📷 Check fingerprint
id: fingerprint
uses: bluesky-social/github-actions/fingerprint-native@main
uses: expo/expo-github-action/fingerprint@main
with:
profile: pull-request
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
- name: 👀 Debug fingerprint
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ steps.fingerprint.outputs.includes-changes }}
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
with:
header: fingerprint-diff
message: |
The Pull Request introduced fingerprint changes against the base commit:
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
```json
${{ steps.fingerprint.outputs.diff }}
${{ steps.fingerprint.outputs.fingerprint-diff }}
```
</details>
@@ -139,7 +152,7 @@ jobs:
- name: 💬 Delete comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ !steps.fingerprint.outputs.includes-changes }}
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
header: fingerprint-diff
delete: true
-3
View File
@@ -115,6 +115,3 @@ src/locale/locales/**/*.js
*.apk
*.aab
*.ipa
# ogcard assets
bskyogcard/src/assets/fonts/noto-*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
yarn lint-staged
-1
View File
@@ -12,4 +12,3 @@
android
ios
src/locale/locales
lib/react-compiler-runtime
+2 -11
View File
@@ -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 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
-41
View File
@@ -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
-41
View File
@@ -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
-79
View File
@@ -1,79 +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/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
-6
View File
@@ -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
+4 -4
View File
@@ -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)
-2
View File
@@ -1,2 +0,0 @@
flows:
- "flows/*"
-30
View File
@@ -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"
-87
View File
@@ -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"
-37
View File
@@ -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"
-206
View File
@@ -1,206 +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:
id: "listsEmpty"
- 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: "Shows the curatelist on my profile"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- 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"
-92
View File
@@ -1,92 +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
- tapOn:
id: "bottomBarProfileBtn"
- 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"
-64
View File
@@ -1,64 +0,0 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: ?users&follows&posts&feeds
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- 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"
- assertVisible:
id: "likeCount"
text: "1"
- tapOn:
id: "likeBtn"
- assertNotVisible:
id: "likeCount"
- tapOn:
label: "Can repost posts"
id: "repostBtn"
- tapOn: "Repost"
- assertVisible:
id: "repostCount"
text: "1"
- tapOn:
id: "repostBtn"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount"
- tapOn:
label: "Can delete posts"
id: "postDropdownBtn"
childOf:
id: "feedItem-by-alice.test"
- tapOn: "Delete post"
- tapOn: "Delete"
-26
View File
@@ -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"
-45
View File
@@ -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%"
-28
View File
@@ -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%"
-124
View File
@@ -1,124 +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
- 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"
-37
View File
@@ -1,37 +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
- 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"
-22
View File
@@ -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"
-31
View File
@@ -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"
-82
View File
@@ -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"
-84
View File
@@ -1,84 +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
- tapOn: "Thread root"
- assertVisible: "Thread reply"
# Can like the root post
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertVisible:
id: "likeCount-expanded"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertNotVisible:
id: "likeCount-expanded"
# Can like a reply post
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertVisible:
id: "likeCount"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertNotVisible:
id: "likeCount"
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"
+12
View File
@@ -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"
-11
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
// eslint-disable-next-line
http.post('http://localhost:1986/' + SERVER_PATH, {
headers: {'Content-Type': 'text/plain'},
body: '',
})
+111
View File
@@ -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()
})
})
+39
View File
@@ -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()
})
})
+211
View File
@@ -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('Upload from 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 Avatar')).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()
})
})
+110
View File
@@ -0,0 +1,110 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} 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()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// 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 element(by.id('com.atproto.moderation.defs#reasonSpam')).tap()
// await element(by.id('sendReportBtn')).tap()
// })
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()
})
})
+47
View File
@@ -0,0 +1,47 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} 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()
})
})
+23
View File
@@ -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')
})
})
+163
View File
@@ -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')
})
})
+189
View File
@@ -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()
})
})
+196
View File
@@ -0,0 +1,196 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp, 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('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Upload from 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 Banner')).tap()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove Avatar')).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()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// 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()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// 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()
// })
})
+25
View File
@@ -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()
})
})
+36
View File
@@ -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()
})
})
+33
View File
@@ -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()
})
})
+103
View File
@@ -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')
})
})
+131
View File
@@ -0,0 +1,131 @@
/* eslint-env detox/detox */
import {beforeAll, describe, it} from '@jest/globals'
import {expect} from 'detox'
import {createServer, loginAsAlice, openApp} 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()
})
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// 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()
// })
// TODO skipping because the test env PDS isnt setup correctly to handle the report -prf
// 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
View File
@@ -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))
+2 -27
View File
@@ -1,10 +1,9 @@
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
import {
downloadAndResize,
DownloadAndResizeOpts,
} from '../../src/lib/media/manip'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
describe('downloadAndResize', () => {
const errorSpy = jest.spyOn(global.console, 'error')
@@ -31,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(),
})
@@ -86,7 +84,6 @@ describe('downloadAndResize', () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image'),
info: jest.fn().mockReturnValue({status: 200}),
flush: jest.fn(),
})
@@ -121,26 +118,4 @@ describe('downloadAndResize', () => {
{mode: 'cover'},
)
})
it('should return undefined for non-200 response', 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(),
})
const opts: DownloadAndResizeOpts = {
uri: 'https://example.com/image',
width: 100,
height: 100,
maxSize: 500000,
mode: 'cover',
timeout: 10000,
}
const result = await downloadAndResize(opts)
expect(errorSpy).not.toHaveBeenCalled()
expect(result).toBeUndefined()
})
})
+92 -321
View File
@@ -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)
})
})
+3 -4
View File
@@ -1,10 +1,10 @@
import {describe, expect, it} from '@jest/globals'
import {it, describe, expect} from '@jest/globals'
import {
isPossiblyAUrl,
isTrustedUrl,
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
isTrustedUrl,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
@@ -170,7 +170,6 @@ describe('isTrustedUrl', () => {
['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) => {
+9 -61
View File
@@ -35,19 +35,15 @@ module.exports = function (config) {
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
const DIST_BUILD_NUMBER =
PLATFORM === 'android'
? process.env.BSKY_ANDROID_VERSION_CODE
: process.env.BSKY_IOS_BUILD_NUMBER
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
@@ -55,10 +51,6 @@ module.exports = function (config) {
: undefined
const UPDATES_ENABLED = !!UPDATES_CHANNEL
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
IS_DEV ? 'dev' : ''
}`
return {
expo: {
version: VERSION,
@@ -73,8 +65,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',
@@ -92,7 +82,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,
@@ -100,29 +90,6 @@ 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',
@@ -184,6 +151,7 @@ module.exports = function (config) {
checkAutomatically: 'NEVER',
channel: UPDATES_CHANNEL,
},
assetBundlePatterns: ['**/*'],
plugins: [
'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
@@ -191,7 +159,7 @@ module.exports = function (config) {
'expo-build-properties',
{
ios: {
deploymentTarget: '15.1',
deploymentTarget: '13.4',
newArchEnabled: false,
},
android: {
@@ -208,20 +176,13 @@ module.exports = function (config) {
{
icon: './assets/icon-android-notification.png',
color: '#1185fe',
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
'expo-video',
'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',
].filter(Boolean),
extra: {
eas: {
@@ -238,19 +199,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',
},
],
},
},
@@ -269,7 +217,7 @@ module.exports = function (config) {
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: SENTRY_DIST,
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
},
},
],
BIN
View File
Binary file not shown.
BIN
View File
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="M5 11a7 7 0 0 1 14 0c0 2.625-1.547 5.138-3.354 7.066a17.23 17.23 0 0 1-2.55 2.242 8.246 8.246 0 0 1-.924.577 2.904 2.904 0 0 1-.172.083 2.904 2.904 0 0 1-.172-.083 8.246 8.246 0 0 1-.923-.577 17.227 17.227 0 0 1-2.55-2.242C6.547 16.138 5 13.625 5 11Zm6.882 10.012Zm.232-.001h-.003a.047.047 0 0 0 .007.001l-.004-.001ZM12 2a9 9 0 0 0-9 9c0 3.375 1.953 6.362 3.895 8.434a19.216 19.216 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: 997 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.122 5.122 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.121 5.121 0 0 1 8.085 5.32c.777-.095 1.617-.04 2.518.172a4.011 4.011 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.124 3.124 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

@@ -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 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 281 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="M21 12a1 1 0 0 1-.293.707l-6 6a1 1 0 0 1-1.414-1.414L17.586 13H4a1 1 0 1 1 0-2h13.586l-4.293-4.293a1 1 0 0 1 1.414-1.414l6 6A1 1 0 0 1 21 12Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 284 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 392 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 369 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 384 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="M6.17 5.004c-.553-.029-.814.107-.937.23-.122.122-.258.383-.23.936.03.552.222 1.28.611 2.15.282.628.655 1.304 1.112 2.005a28.26 28.26 0 0 1 1.72-1.88 28.258 28.258 0 0 1 1.88-1.719 14.886 14.886 0 0 0-2.007-1.112c-.868-.39-1.597-.581-2.15-.61Zm5.83.44c-.985-.688-1.953-1.247-2.863-1.655-.998-.447-1.978-.736-2.863-.782-.885-.047-1.791.148-2.455.812-.664.664-.859 1.57-.812 2.455.046.885.335 1.865.782 2.863.408.91.967 1.878 1.655 2.863-.688.985-1.247 1.953-1.655 2.863-.447.998-.736 1.978-.782 2.863-.047.885.148 1.791.812 2.455.664.663 1.57.859 2.455.812.885-.046 1.865-.335 2.863-.782.91-.408 1.878-.967 2.863-1.655.985.688 1.952 1.247 2.863 1.655.998.447 1.978.736 2.863.782.885.047 1.791-.148 2.455-.812.663-.664.859-1.57.812-2.455-.046-.885-.335-1.865-.782-2.863-.408-.91-.967-1.878-1.655-2.863.688-.985 1.247-1.952 1.655-2.863.447-.998.736-1.978.782-2.863.047-.885-.148-1.791-.812-2.455-.664-.664-1.57-.859-2.455-.812-.885.046-1.865.335-2.863.782-.91.408-1.878.967-2.863 1.655Zm0 2.497A25.9 25.9 0 0 0 9.86 9.86 25.899 25.899 0 0 0 7.94 12c.569.711 1.211 1.431 1.92 2.14.709.709 1.429 1.351 2.14 1.92a25.925 25.925 0 0 0 2.14-1.92A25.925 25.925 0 0 0 16.06 12a25.921 25.921 0 0 0-1.92-2.14A25.904 25.904 0 0 0 12 7.94Zm5.274 2.384a28.232 28.232 0 0 0-1.72-1.88 28.27 28.27 0 0 0-1.88-1.719 14.89 14.89 0 0 1 2.007-1.112c.868-.39 1.597-.581 2.15-.61.552-.029.813.107.936.23.123.122.258.383.23.936-.03.552-.222 1.28-.611 2.15a14.883 14.883 0 0 1-1.112 2.005Zm0 3.35a28.24 28.24 0 0 1-1.72 1.88 28.24 28.24 0 0 1-1.88 1.719c.702.457 1.378.83 2.007 1.112.868.39 1.597.581 2.15.61.552.03.813-.106.936-.23.123-.122.258-.383.23-.935-.03-.553-.222-1.282-.611-2.15a14.888 14.888 0 0 0-1.112-2.006Zm-6.949 3.599a28.23 28.23 0 0 1-1.88-1.72 28.27 28.27 0 0 1-1.719-1.88 14.89 14.89 0 0 0-1.112 2.007c-.39.868-.581 1.597-.61 2.15-.029.552.107.813.23.936.122.123.383.258.936.23.552-.03 1.28-.222 2.15-.611a14.884 14.884 0 0 0 2.005-1.112Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 2.0 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 2a7.307 7.307 0 0 0-7.298 6.943l-.19 3.798-1.321 2.641A1.809 1.809 0 0 0 4.809 18H7.1a5.002 5.002 0 0 0 9.8 0h2.291a1.809 1.809 0 0 0 1.618-2.618l-1.32-2.641-.19-3.798A7.308 7.308 0 0 0 12 2Zm0 18a3.001 3.001 0 0 1-2.83-2h5.66A3.001 3.001 0 0 1 12 20Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 398 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="M4.702 8.943a7.307 7.307 0 0 1 14.596 0l.19 3.798 1.321 2.641A1.809 1.809 0 0 1 19.191 18H16.9a5.002 5.002 0 0 1-9.8 0H4.809a1.809 1.809 0 0 1-1.618-2.618l1.32-2.641.19-3.798ZM9.17 18a3.001 3.001 0 0 0 5.658 0H9.171ZM12 4a5.307 5.307 0 0 0-5.3 5.042l-.19 3.798a2 2 0 0 1-.21.795L5.119 16h13.764l-1.183-2.365a2 2 0 0 1-.208-.795l-.19-3.798A5.308 5.308 0 0 0 12 4Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 505 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="m19.785 8.815 1.034 7.761L7.595 3.352a7.853 7.853 0 0 1 12.19 5.463ZM4 19h3.354c.904 1.748 2.607 3 4.646 3 2.038 0 3.742-1.252 4.646-3h.94l2.707 2.707a1 1 0 0 0 1.414-1.414l-18-18a1 1 0 0 0-1.414 1.414l2.666 2.666a7.842 7.842 0 0 0-.743 2.442l-1.207 9.053A1 1 0 0 0 4 19Zm8 1c-.823 0-1.613-.363-2.222-1h4.443c-.608.637-1.398 1-2.221 1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 478 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="M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19h-.94c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053c.116-.87.372-1.69.743-2.442L2.293 3.707a1 1 0 0 1 0-1.414Zm4.19 5.604c-.134.376-.23.772-.285 1.183L5.142 17h10.444L6.483 7.897ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM8.834 2.666a7.853 7.853 0 0 1 10.95 6.15l.645 4.832a1 1 0 0 1-1.983.265l-.644-4.833A5.853 5.853 0 0 0 9.64 4.495a1 1 0 0 1-.807-1.83Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 628 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="M12 2a7.853 7.853 0 0 0-7.784 6.815l-1.207 9.053A1 1 0 0 0 4 19h3.354c.904 1.748 2.607 3 4.646 3 2.039 0 3.742-1.252 4.646-3H20a1 1 0 0 0 .991-1.132l-1.207-9.053A7.853 7.853 0 0 0 12 2Zm2.222 17H9.778c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 387 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="M4.216 8.815a7.853 7.853 0 0 1 15.568 0l1.207 9.053A1 1 0 0 1 20 19h-3.354c-.904 1.748-2.607 3-4.646 3-2.039 0-3.742-1.252-4.646-3H4a1 1 0 0 1-.991-1.132l1.207-9.053ZM9.778 19c.61.637 1.399 1 2.222 1s1.613-.363 2.222-1H9.778ZM12 4a5.853 5.853 0 0 0-5.802 5.08L5.142 17h13.716l-1.056-7.92A5.853 5.853 0 0 0 12 4Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 454 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M19.002 3a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h14Z"/></svg>

Before

Width:  |  Height:  |  Size: 221 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="M2.002 6a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2h-1a3 3 0 0 1-3-3V6Zm3-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h7a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-14Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 386 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="M2.002 7a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H12.28l-4.762 2.858A1 1 0 0 1 6.002 21v-2a4 4 0 0 1-4-4V7Zm4-2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1a1 1 0 0 1 1 1v1.234l3.486-2.092a1 1 0 0 1 .514-.142h6a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-12Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 381 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 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm0 10a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm10-1a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2h-7Zm-1-9a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 302 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="M6 6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM3 7a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Zm-6 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3 1a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm9 0a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 381 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm11.543 7.293a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.242 1 1 0 0 0-1.414-1.414 1 1 0 0 1-1.414-1.414Zm-6 0a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.243 1 1 0 0 0-1.414-1.415 1 1 0 0 1-1.414-1.414Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 440 B

@@ -1 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Zm10.957 6.293a1 1 0 1 0 0 1.414 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414Zm-6.331-.22a1 1 0 1 0 .331 1.634 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414.994.994 0 0 0-.331-.22Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 443 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="M13.832 1.014a1 1 0 0 1 1.154.818L14 2l.986-.168v.003l.001.005.002.014.007.045.021.156c.017.13.035.312.048.527.025.42.028 1.01-.082 1.6-.127.69-.447 1.31-.701 1.726a7.107 7.107 0 0 1-.498.712l-.01.014-.004.005-.002.001v.002L13 6l.767.642a1 1 0 0 1-1.535-1.282l.002-.003.017-.02a5.13 5.13 0 0 0 .324-.47c.198-.325.378-.705.442-1.049.068-.37.071-.78.051-1.12a6.268 6.268 0 0 0-.05-.504l-.004-.025m.818-1.155a1 1 0 0 0-.818 1.155Zm5.257 1.545a1 1 0 0 1 .602 1.28l-.45 1.25a1 1 0 0 1-1.882-.678l.45-1.25a1 1 0 0 1 1.28-.602ZM6.524 7.136a2 2 0 0 1 3.294-.732l7.778 7.778a2 2 0 0 1-.732 3.294L4.653 21.91c-1.596.579-3.142-.967-2.563-2.563L6.524 7.136Zm9.658 8.46L8.404 7.818 3.97 20.03l12.212-4.434Zm5.712-8.543a1 1 0 0 1-.447 1.341l-1 .5a1 1 0 1 1-.894-1.788l1-.5a1 1 0 0 1 1.341.447Zm-4.687-.26a1 1 0 0 1 0 1.414l-1 1a1 1 0 1 1-1.414-1.414l1-1a1 1 0 0 1 1.414 0Zm-.206 4.165A1 1 0 0 1 18.042 10L18 11l.042-1 .003.001h.014l.035.003.117.008a7.693 7.693 0 0 1 1.594.306c.423.135.861.352 1.168.516a11.873 11.873 0 0 1 .508.288l.032.02.01.006.004.002L21 12l.527-.85a1 1 0 0 1-1.054 1.7l-.005-.003-.023-.014a7.477 7.477 0 0 0-.415-.236 5.497 5.497 0 0 0-.835-.374A5.684 5.684 0 0 0 17.973 12l-.015-.002h-.002a1 1 0 0 1-.955-1.041Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z"/></svg>

Before

Width:  |  Height:  |  Size: 450 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="M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 775 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="M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 821 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="M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 466 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="M7 2a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1ZM4 9a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2h.5a3.5 3.5 0 1 1 0 7H18v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9Zm12 0H6v11h10V9Zm2 5h.5a1.5 1.5 0 0 0 0-3H18v3Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 436 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="M6 2a1 1 0 0 1 1 1v2h11a1 1 0 0 1 1 1v11h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H6a1 1 0 0 1-1-1V7H3a1 1 0 0 1 0-2h2V3a1 1 0 0 1 1-1Zm1 5v10h10V7H7Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 289 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="M4.4 9.493C4.14 10.28 4 11.124 4 12a8 8 0 1 0 10.899-7.459l-.953 3.81a1 1 0 0 1-.726.727l-3.444.866-.772 1.533a1 1 0 0 1-1.493.35L4.4 9.493Zm.883-1.84L7.756 9.51l.44-.874a1 1 0 0 1 .649-.52l3.306-.832.807-3.227a7.993 7.993 0 0 0-7.676 3.597ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8.43.162a1 1 0 0 1 .77-.29l1.89.121a1 1 0 0 1 .494.168l2.869 1.928a1 1 0 0 1 .336 1.277l-.973 1.946a1 1 0 0 1-.894.553h-2.92a1 1 0 0 1-.831-.445L9.225 14.5a1 1 0 0 1 .126-1.262l1.08-1.076Zm.915 1.913.177-.177 1.171.074 1.914 1.286-.303.607h-1.766l-1.194-1.79Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 714 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="M17.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-9 9A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l9-9ZM10 12.414V14h1.586l8-8L18 4.414l-8 8ZM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 393 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="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-5a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm-5.894 7.803a1 1 0 0 1 1.341-.447c1.719.859 3.387.859 5.106 0a1 1 0 1 1 .894 1.788c-2.281 1.141-4.613 1.141-6.894 0a1 1 0 0 1-.447-1.341Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 489 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="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 2ZM9.351 12.13c1.898-1.507 2.176-2.95 1.613-3.83a1.524 1.524 0 0 0-1.225-.707 1.562 1.562 0 0 0-1.218.53 1.561 1.561 0 0 0-1.326-.082c-.456.186-.8.588-.91 1.083-.227 1.02.527 2.282 2.826 3.048a.256.256 0 0 0 .24-.043Zm5.538.042c2.299-.766 3.053-2.027 2.826-3.048a1.524 1.524 0 0 0-.91-1.082 1.561 1.561 0 0 0-1.326.081 1.562 1.562 0 0 0-1.217-.53 1.524 1.524 0 0 0-1.226.706c-.563.881-.285 2.325 1.613 3.83.068.054.158.07.24.043Zm1.072 2.38a4 4 0 0 1-7.924 0c-.04-.293.218-.525.514-.499 2.309.206 4.587.206 6.896 0 .296-.026.555.206.514.5Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 786 B

Some files were not shown because too many files have changed in this diff Show More