書き換え箇所が多いのでメモしておきます。Vitestも対象。
また、Options APIで書いたMixinは使えなくなるのと、そもそもMixinは唐突に現れる為、可読性が悪かったので、Utilでexportして、importして使うようにします。
同様にInjectも便利なのですが、やはり唐突感があるので、役割が明確な$authや$toast以外のはUtil移動します。

Composable(use始り)も選択肢でしたが、テストを書く時に、Mockにして、テスト範囲を狭め場合に適している。
UtilはMock作らずに動かしちゃいたい場合に適している。
ただ、Nuxtは両者とも自動インポートされますが、VitestやJestではされないので、明示的にimportする必要があります。結果的にimport文があるので、唐突感がなくなって良き。

script + defineNuxtComponent -> script setup

<script setup><script> + setup() の2パターンあります。
setup()内で早期Returnできないので、ネストが深くなる。
結局functionを作る事になるので、前者にしました。
また、returnで値を公開する必要があるので、高機能だけど手間が掛かる。

- <script>
- export default defineNuxtComponent({
+ <script setup lang="ts">

- })
</script>

Composition API: setup() | Vue.js

単一ファイルコンポーネントで Composition API を使用する場合は、
より簡潔で人間工学的な構文のために、<script setup> を強くお勧めします。

components, mixins -> 削除

Composition APIでは不要なので削除。
importはNuxtでは命名規則に従っていれば不要ですが、Vitestでは自動インポートできないので、引き続き残しています。

<script setup lang="ts">
import AppLoading from '~/components/app/Loading.vue'
- import Application from '~/utils/application.js'

-  components: {
-    AppLoading
-  },
-  mixins: [Application],

props -> defineProps

template内のみで参照する場合

<script setup lang="ts">
- props: {
+ defineProps({
  alert: {
    type: String,
    default: ''
  }
- }
+ })

script内でも参照する場合

上記の3行目を下記に変更。$props.alert等で参照可能になります。

+ const $props = defineProps({

computed + get/set -> const computed + get/set

親から受け取った値が変更されたら、親に渡して変更して受け取る。この流れは変わらない。
短く書けるようになったのは良いですね。

-  computed: {
-    syncQuery: {
-      get () {
-        return this.query
-      },
-      set (value) {
-        this.$emit('update:query', value)
-      }
-    }
-  },
+ const syncQuery = computed({
+   get: () => $props.query,
+   set: (value: object) => $emit('update:query', value)
+ })

$refs -> const + defineExpose

<script setup>内のfunctionはそのままでは参照できないので、defineExposeで参照できるようにしてあげる必要があります。
親から呼ばれる事を明示する事になるので、唐突感がなくなり解りやすい。

親(PageやComponent)

refは短い名前にしていましたが、constで定義する事になりスコープが広くなるので、少し長い名前に変えました。

        <SpacesSearch
-          ref="search"
+          ref="spacesSearch"

<script setup lang="ts">
+ const spacesSearch = ref<any>(null)

-    this.$refs.search.setError()
+    spacesSearch.value.setError()
        <InvitationsUpdate
-          ref="update"
+          ref="invitationsUpdate"

-            @show-update="$refs.update.showDialog($event)"
+            @show-update="invitationsUpdate.showDialog($event)"

<script setup lang="ts">
+ const invitationsUpdate = ref<any>(null)

子(Component)

<script setup lang="ts">
+ defineExpose({ setError })

function setError () {
<script setup lang="ts">
+ defineExpose({ showDialog })

async function showDialog (item: any) {

$emit -> defineEmits

template内の$emitは使用可能ですが、lintエラーになるのでdefineEmitsを追加する事になる。
定義するなら参照した方が分かりやすそう。

    @update:model-value="$emit('update:alert','')"

<script setup lang="ts">
+- defineEmits(['update:alert'])
+ const $emit = defineEmits(['update:alert'])

-> 使用するComposable(use始まり)を定義

useNuxtApp()で、Plugin等でProvideしているのをInjectできます。
※Vitestではそれぞれvi.stubGlobalで値を返してあげる必要があります。

const $config = useRuntimeConfig()
const { t: $t, tm: $tm } = useI18n()
const { $auth, $toast } = useNuxtApp()
const $route = useRoute()

data + return -> const + ref

- data () {
-   return {
-     loading: true,
-     processing: false,
-     waiting: false,
-     dialog: false,
-     tabDescription: 'input',
-     infomation: null,
-     uid: null
-   }
- },
+ const loading = ref(true)
+ const processing = ref(false)
+ const waiting = ref(false)
+ const dialog = ref(false)
+ const tabDescription = ref('input')
+ const infomation = ref<any>(null)
+ const uid = ref<string | null>(null)

refの値に、script内で参照・変更するには.valueを付ける必要がある。template内では不要。
リアクティビティー API: コア | Vue.js

【コラム】nullを使うかundefinedを使うか?

極論、どちらでも良いのですが、個人的には、undefinedを書いたり、typeofでundefined判定するのを避けています。
undefined(未定義)入れ物がないnull入れ物はあるけど値がないなので。

とは言え、undefinedの値は返ってきます。nullは明示するかAPIレスポンスに存在する。
undefinedでもnullでもないは、下記で判定でき、短く書けるので、好んで使っています。

    if (data != null) {

また、上で追加した、これ↓では初期値に{}''は入れず、nullにする事で、取得していない事が明確になるようにしています。ここでは別途、状態(loading)を持っているので、どちらでも動きますが、それぞれに状態を持った方が良いと思います。

+ const infomation = ref<any>(null)
+ const uid = ref<string | null>(null)

例えば、こんなケース。このコードではlocaleが設定されれば問題ないのですが、もしエラー時にalertに''がセットされてしまったら、エラー処理がされず、trueが返ってしまいます。

-  let alert = ''
+  let alert: string | null = null
  if (response?.ok) {
    if (data?.infomation?.current_page === page.value) {
      <成功時の処理>
    } else {
      alert = $t('system.error')
    }
  } else {
    if (data == null) {
      alert = $t(`network.${response?.status == null ? 'failure' : 'error'}`)
    } else {
      alert = data.alert || $t('system.default')
    }
  }
-  if (alert !== '') {
+  if (alert != null) {
    <エラー処理>
  }

-  return alert === ''
+  return alert == null

逆に、null(or undefined)と''が同じ振る舞いをする場合は、''の方がシンプルです。

-    <v-card-text v-if="alert != null && alert !== ''">
+    <v-card-text v-if="alert !== ''">
      <v-icon color="warning">mdi-alert</v-icon>
      {{ alert }}
    </v-card-text>

<script setup lang="ts">
- const alert = ref<string | null>(null)
+ const alert = ref('')

computed -> const + computed

computedも同様に、script内で参照するには.valueを付ける必要がある。template内では不要。
また、全体的にですが、this.は使えなくなるので、削除もしています。

1行で書く(パラメータなし)

-  computed: {
-    invitationURL () {
-      return `${location.protocol}//${location.host}/users/sign_up?code=${this.invitation.code}`
-    }
-  },
+ const invitationURL = computed(() => `${location.protocol}//${location.host}/users/sign_up?code=${invitation.value.code}`)

1行で書く(パラメータあり)

-  computed: {
-    currentMemberAdmin () {
-      return (space) => {
-        return space?.current_member?.power === 'admin'
-      }
-    }
-  },
+ const currentMemberAdmin = computed(() => (space: any) => space?.current_member?.power === 'admin')

複数行

- computed: {
-   title () {
+ const title = computed(() => {
  let label = ''
-  if (this.infomation?.label_i18n != null && this.infomation.label_i18n !== '') {
+  if (infomation.value?.label_i18n != null && infomation.value.label_i18n !== '') {
-    label = `[${this.infomation.label_i18n}]`
+    label = `[${infomation.value.label_i18n}]`
  }
-  return label + (this.infomation?.title || '')
+  return label + (infomation.value?.title || '')
-   }
- },
+ })

created -> function + 呼び出し

createdの名前は何でもOK。早期Returnを使いたいので、functionを作って呼び出しています。
createdを呼び出す時のawaitは不要です。

- async created () {
+ created()
+ async function created () {
-   if (!await this.getInfomationsDetail()) { return }
+   if (!await getInfomationsDetail()) { return }

-   this.loading = false
+   loading.value = false
- },
+ }

序でにURLパラメータのID([id].vue)のチェック(文字、0始まり)も追加。

-   if (!await getInfomationsDetail()) { return }
+   const id = Number($route.params.id)
+   if (isNaN(id) || String(id) !== String($route.params.id)) { return redirectError(404, {}) }
+   if (!await getInfomationsDetail(id)) { return }

redirectErrorは、Mixin -> Utilでexportしてimport 参照

methods -> function

- methods: {
-   async getInfomationsDetail () {
+ async function getInfomationsDetail () {

-   }
}

Mixin -> Utilでexportしてimport

appCheckResponseとappCheckErrorResponseはMixinのmethodsで定義していたもの。
呼び出しは簡素に書けていたものの、呼び出し先は複雑になってしまい、可読性が悪くなっていました。Utilでやる事を限定する事で、見通しが良くなったと思います。

redirectErrorのimportは自動インポートの為、不要ですがVitestの為に明示しています。

- import Application from '~/utils/application.js'
+ import { redirectError } from '~/utils/auth'

async function getInfomationsDetail (id: number) {
  const url = $config.public.infomations.detailUrl.replace(':id', String(id))
  const [response, data] = await useApiRequest($config.public.apiBaseURL + url)

  if (response?.ok) {
-    if (this.appCheckResponse(data, { redirect: true }, data?.infomation == null)) {
+    if (data?.infomation == null) {
+      redirectError(null, { alert: $t('system.error') })
+    } else {
      infomation.value = data.infomation
      return true
    }
  } else {
-    this.appCheckErrorResponse(response?.status, data, { redirect: true, require: true }, { notfound: true })
+    if (response?.status === 404) {
+      redirectError(404, { alert: data.alert || $t('system.default'), notice: data.notice })
+    } else if (data == null) {
+      redirectError(response?.status, { alert: $t(`network.${response?.status == null ? 'failure' : 'error'}`) })
+    } else {
+      redirectError(response?.status, { alert: data.alert || $t('system.default'), notice: data.notice })
+    }
  }

  return false
}

utils/auth.ts

function redirectError (statusCode: any, query: any) {
  showError({ statusCode, data: { alert: query.alert, notice: query.notice } })
  /* c8 ignore next */ // eslint-disable-next-line no-throw-literal
  if (process.env.NODE_ENV !== 'test') { throw 'showError' }
}

export {
  redirectError
}

.eslintrc.js

  rules: {
+    'no-lonely-if': 'off'
  }

Inject(一部) -> Utilでexportしてimport

-          ({{ $dateFormat('ja', infomation.started_at, 'N/A') }})
+          ({{ dateFormat('ja', infomation.started_at, 'N/A') }})

+ import { dateFormat } from '~/utils/helper'

plugins/utils.js -> utils/helper.ts

- export default defineNuxtPlugin((_nuxtApp) => {
-   return {
-     provide: {
+ export {
      sleep,
      dateFormat,
-     }
-   }
- })
+ }

- export const TestPluginUtils = {
-   $sleep: sleep,
-   $dateFormat: dateFormat
- }

test/setup.ts

- import { TestPluginUtils } from '~/plugins/utils'

config.global.mocks = {

-  },
+  }
-  ...TestPluginUtils
}
vi.stubGlobal('useRuntimeConfig', vi.fn(() => config.global.mocks.$config))
vi.stubGlobal('useI18n', vi.fn(() => ({ t: config.global.mocks.$t, tm: config.global.mocks.$tm })))

Vitest mocks, data, wrapper.vm.$data -> stubGlobal, wrapper.vm[key], wrapper.vm

useNuxtAppやuseRouteを使うように変更したので、
mountで指定していた「mocks: {」は効かなくなります。なので、vi.stubGlobalに変更します。

data () {」も参照先が変更になる($dataではなくなる)ので、
スマートではないですが、直接セットするように変更します。

  const mountFunction = (loggedIn: boolean, query = {}, values = {}) => {
    vi.stubGlobal('useApiRequest', mock.useApiRequest)
    vi.stubGlobal('useAuthRedirect', vi.fn(() => mock.useAuthRedirect))
    vi.stubGlobal('navigateTo', mock.navigateTo)
+    vi.stubGlobal('useNuxtApp', vi.fn(() => ({
+      $auth: {
+        loggedIn,
+        setData: mock.setData
+      },
+      $toast: mock.toast
+    })))
+    vi.stubGlobal('useRoute', vi.fn(() => ({
+      query: { ...query }
+    })))

-    const wrapper = mount(Page, {
+    const wrapper: any = mount(Page, {
      global: {
        stubs: {
          AppProcessing: true,
          AppMessage: true,
          ActionLink: true
-        },
-        mocks: {
-          $auth: {
-            loggedIn,
-            setData: mock.setData
-          },
-          $route: {
-            path: '/users/sign_in',
-            query: { ...query }
-          },
-          $toast: mock.toast
        }
-      },
-      data () {
-        return values
      }
    })
    expect(wrapper.vm).toBeTruthy()
+    for (const [key, value] of Object.entries(values)) { wrapper.vm[key] = value }
    return wrapper
  }

また、wrapper.vm.$dataに入って変数は、直下に変更、
Inject(下記では$dateFormat)はimportに変更したので、$を削除します。
※dateFormatの所は固定値でも良いのですが、別途テストを書いている為、正しさはそっちで担保して、ここでは表示されるかのみ確認しています。

-    expect(wrapper.vm.$data.infomation).toEqual(data.infomation)
+    expect(wrapper.vm.infomation).toEqual(data.infomation)

-    expect(wrapper.text()).toMatch(wrapper.vm.$dateFormat('ja', data.infomation.started_at)) // 開始日
+    expect(wrapper.text()).toMatch(wrapper.vm.dateFormat('ja', data.infomation.started_at)) // 開始日

TypeScriptのlintエラー対応あれこれ

初期値がある場合、ある程度、型推論が効くので、明示する必要はないのですが、推論できない場合があるので、lintで怒られた所だけ対応します。
頑張ると大変だったり、そもそも頑張れないものもあるので、anyで諦めて貰うのが良いと思います。頑張りたいのは、ここではないので。


これは対応できなものらしいので、as anyで逃げます。

型のインスタンス化は非常に深く、無限である可能性があります。ts(2589)
-  for (const item of $tm('items.space')) {
+-  for (const item of ($tm('items.space') as any)) {
+  for (const [, item] of Object.entries($tm('items.space') as any) as any) {

プロパティ 'user' は型 'SessionData' に存在しません。ts(2339)
-        user: computed(() => authData.value?.user),
+        user: computed(() => (authData.value as any)?.user),

プロパティ 'id' は型 '{}' にありませんが、型 'SessionData' では必須です。ts(2741)
-        setData: (data: object) => { authData.value = data },
+        setData: (data: object) => { (authData.value as any) = data },
型 'string' の式を使用して型 '{ admin: string; writer: string; default: string; }'
 にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
型 'string' のパラメーターを持つインデックス シグネチャが型 '{ admin: string; writer: string; default: string; }'
 に見つかりませんでした。ts(7053)
-  <v-icon size="x-small">{{ $config.public.member.powerIcon[power] }}</v-icon>
+  <v-icon size="x-small">{{ ($config.public.member.powerIcon as any)[power] }}</v-icon>

neverは、値を持たない事を意味する型。明示的にanyで対応。

プロパティ 'filter' は型 'never' に存在しません。ts(2339)
パラメーター 'item' の型は暗黙的に 'any' になります。ts(7006)
-  return items.filter(item => !item.adminOnly || $props.admin)
+  return items.filter((item: any) => !item.adminOnly || $props.admin)

値は文字列だけど、何故、数字だと推論されたのだろうか?

型 'number' を型 'string' に割り当てることはできません。ts(2322)
-    :label="label"
+    :label="String(label)"

こちらも同様に文字列に変換で対応。

この呼び出しに一致するオーバーロードはありません。
  前回のオーバーロードにより、次のエラーが発生しました。ts(2769)
-  const url = $config.public.spaces.detailUrl.replace(':code', $route.params.code)
+  const url = $config.public.spaces.detailUrl.replace(':code', String($route.params.code))

頑張らずにanyで対応。

プロパティ 'showItems' は型 '{ <省略> (......' に存在しません。ts(2339)
-    const wrapper = mountFunction(true)
+    const wrapper: any = mountFunction(true)

    expect(wrapper.vm.showItems).toEqual(requiredItems)

今回のコミット内容

今回記載した箇所にはありませんが、
以前は、template内でオプショナルチェーン (?.)を書くとテストで落ちましたが、
Vitestになってから(?)落ちなくなったので、簡素に書けて良いです。
とは言え、上手く使わないとバグの温床になるし、無駄に使うと解りにくくなりますが。

origin#507 Options API/JavaScriptをComposition API/TypeScriptに書き換え。Mixinを止める。injectをUtilに書き換え
origin#507 Options API/JavaScriptをComposition API/TypeScriptに書き換え。Mixinを止める。injectをUtilに書き換え

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です